On this page
Alignment check — 2026-10-04: internal RPCAlignment check — 2026-10-03Trace one activityWhere the mapping breaksReview against the upstream architecture assetsHow closely does this model match Temporal?
Start with the architecture map for startup, service boundaries, and transport status.
The ownership split is useful; the implementation is not a scaled-down copy. Read the toy to learn which component makes a decision, then follow the corresponding OSS path below. A class name here is a conceptual counterpart, not a promise that the Go package has the same shape.
| Toy component | Pinned production symbol | Owned state | Incoming → outgoing calls | Preserved invariant | Simplification |
|---|---|---|---|---|---|
Frontend |
WorkflowHandler |
Public request validation, no execution state | SDK → HistoryClient / MatchingClient RPCs |
Workers enter through Frontend; Frontend never reads execution state itself | Generic gRPC bridge over generated schemas; no auth or quotas |
HistoryClient, MatchingClient |
client/history, client/matching |
Cached connections per host | Caller → shard / partition owner | Routing keys come from the request (generated (routing) options, task tokens, partition names) |
Retries only ShardOwnershipLost redirects; no backoff policy |
ShardController |
shard/controller_impl.go |
Shard leases (range IDs) held by this host | Membership → acquire / release; failed fence → release | A write with a replaced range ID cannot commit | Lazy acquisition; no shard context caches |
History |
service/history and ExecutionManager |
Execution, history, attempts, timers, transfer intent | historyservice RPCs → fenced persistence | History decides whether a task is current and whether an attempt can complete | Event vocabulary and mutable state collapsed; public events built at the RPC boundary |
QueueProcessor |
History transfer task executor | Pending transfer/timer/visibility work of assigned shards | History store → AddWorkflowTask / AddActivityTask |
Retryable transfer failure retains intent | One loop per host over its shards; no per-shard queues, acks, or backoff |
Matching |
matchingEngine |
Durable delivery records, waiting polls | matchingservice RPCs → RecordWorkflowTaskStarted / RecordActivityTaskStarted; parent partitions |
Start is recorded before delivery; the poll response is built from History's answer | Persisted range fencing and acknowledgement recovery; small dispatch policies |
Partitions |
Partition manager and physical queue manager | Loaded managers, generations, gauges, poll scope | Namespace/membership callbacks → stop/reload | Unload interrupts old polls and clears process state while backlog survives | Unloads immediately on membership change, without upstream's propagation delay |
UserData |
userDataManagerImpl |
Root durable family config, child caches | Root update → parent/child propagation | Only root writes; child may lag | Manual propagation; persistence enforces expected versions |
Metrics |
metrics.Handler and metricstest.CaptureHandler |
Latest gauge value per complete tag set | Partition load → handler with fixed tags; backlog reports and Stop → samples |
A sample carries the tags its handler was built with, not the current namespace state | One process-wide capture; gauges only, no counters, timers, or export |
Membership |
Monitor/ServiceResolver, static membership |
Host lists, announced addresses, owner generation | Host change → shard release, partition unload | Host placement is separate from SDK worker registration | Modulo hash rather than a ring; no gossip or health |
ApplicationWorker |
User-hosted TypeScript SDK Worker | User code and in-flight work | Frontend poll → Frontend completion (public messages) | Workflow replay and activity execution occur outside server services | Generator runtime replaying public history, or the stock SDK over gRPC |
Visibility |
Visibility manager interfaces | Read model | History projection → Frontend list | Reads can lag History | No independent OSS visibility service matching this class |
InternalWorker |
service/worker |
Toy scan report | Worker service lifetime → typed stores | Background work is distinct from SDK workers | Scanner does not model the actual jobs |
Alignment check — 2026-10-04: internal RPC
Findings 4 and 5 below are resolved.
- Roles call each other only over RPC. Requests and responses are upstream's generated
historyserviceandmatchingservicemessages (api/, fromproto/internalat39bffa99), sent as proto3 JSON over Effect RPC. No role imports another role; the dependency rules enforce it. Shared-process and separate-process deployments use the same calls (pnpm start --service history). - History shards are leased. Each History host takes a shard's range-ID lease before serving it, and every execution write is fenced by that lease in memory and PostgreSQL (migration 005). A replaced lease fails the old host's write with
ShardOwnershipLost, which names the new owner;HistoryClientretries there.test/cluster.test.tsandtest/postgres.integration.tsexercise both paths. - Matching routes and forwards across hosts.
MatchingClientroutes by partition owner. Forwarded adds and polls useAddWorkflowTaskwithforward_infoandPollWorkflowTaskQueuewithforwarded_source, and a forwarded poll waits at the root host. Cancelling the caller interrupts every hop. - Poll responses come from History's start response.
RecordWorkflowTaskStartedreturns the workflow type, event IDs, and history;RecordActivityTaskStartedreturns the scheduled event. Matching builds the poll response and task token from them; Frontend only translates. Matching task records no longer carry activity names or inputs.
Remaining differences: the internal transport is Effect RPC framing, not gRPC; tokens are proto3 JSON rather than protobuf bytes; History keeps its internal event log and builds public events at its boundary, so tokens carry internal scheduled-event IDs. Membership is static and hashes by modulo; there is no host failure detection or graceful handoff. The toy uses namespace names where upstream uses namespace IDs.
Alignment check — 2026-10-03
Reviewed tiny-temporal 8a8b9fb against the local Temporal checkout at
39bffa99f0e0252fefb794f6a0cb571464187fca.
The DeepWiki architecture overview
was used for orientation; the pinned Go implementation is the authority for the comparisons below.
This is an architectural review, not a claim of feature or database compatibility.
Assessment: the service responsibilities and top-level layout align. The three reproduced Matching gaps below are now corrected, with regression tests in memory and PostgreSQL. Internal deployment/shard ownership and poll-response assembly remain simplified.
1. Deployment routing — corrected
History now supplies a version directive with each transfer. Matching stores auto tasks in the default physical queue and chooses the deployment when dispatching, using the routing visible to that partition. Pinned tasks remain in their version's queue. Matching no longer reads the whole execution to select a deployment.
This follows upstream's separation of immediate dispatch and backlog storage in
AddTask
and queue selection.
test/versioning.test.ts backlogs work under v1, changes the
current deployment to v2, and verifies auto work reaches v2 while pinned work reaches v1.
It also checks stale activity-partition routing until propagation. The PostgreSQL integration
repeats deployment selection after rebuilding all server layers. Migration 004 and memory
snapshot restore upgrade old version-bound backlog without overwriting destination task IDs.
Limits: this teaches storage versus dispatch routing, not the complete worker-versioning
protocol. History.setVersioning is a lab control that updates outstanding transfer directives;
it does not invalidate already-spooled or running tasks. Public SDK deployment/versioning APIs
and production version-transition validation are not implemented.
2. Child backlog ownership and forwarding — corrected
Normal child partitions now have their own matcher and durable backlog. Adds try local and parent sync matching; an unsuccessful forward spools only at the originating child. Backlog readers retry parent matching and acknowledge the origin after History accepts delivery. Forwarded polls share one completion and deadline across all hops; delivery, cancellation, or unload removes every registration. Unsupported sticky operations are rejected.
This preserves the ownership rule in upstream
ForwardTask
and forwarded-task fallback.
test/polling.test.ts verifies child retention, retryable start
failure, ancestor delivery, multi-hop poll cleanup, and sticky rejection. PostgreSQL tests
rediscover child backlog after restart with only a root poller.
Limits: parent selection uses a fixed binary tree; forwarding was then local Effect calls (RPC since 2026-10-04). The reader scans stored queues rather than running distributed, paginated readers. Ordinary SDK calls still use the root partition; partition selection is an internal lab API.
3. Persistent user-data version checks — corrected
Both stores now compare the expected version during the write. PostgreSQL uses conditional
insert/update and reports UserDataConflict when another manager wins. The local semaphore
still protects one manager's cache and migration flags; the database comparison protects
against independent managers. This follows upstream
task_user_data.go.
test/user-data-concurrency-contract.ts synchronizes
two independent managers after reading version 0 and requires exactly one successful write.
It also races updates to an existing row. The contract runs against memory and separate
PostgreSQL pools.
Limits: the draft CHASM exercise still writes two stores sequentially. Each write checks its version, but the pair is not atomic and can require reconciliation after a partial failure. It does not model CHASM replication or distributed rollout.
4. Deployment and shard boundaries are conceptual — resolved 2026-10-04, see above
One entrypoint hosting several roles is consistent with upstream's
DefaultServices.
However, upstream keeps internal service RPC boundaries even when roles share a process.
The toy directly injects History and Matching; only public Frontend calls cross a network.
Four recorded History shard labels share one engine and one SQL partition lock. Membership
host labels do not start or route to independent hosts.
This is adequate for tracing responsibilities, but does not exercise internal protobuf contracts, per-shard engines/leases, remote errors, retries, or independent service deployment. Network listeners alone would not supply those semantics.
5. Poll-response assembly sits in a different role — resolved 2026-10-04, see above
The toy's former workflow-service.ts obtained a task
from Matching, then read the full execution through Frontend and constructed the poll response.
Upstream Matching records the start in History and builds its internal poll response from the
returned state/history; Frontend translates that response to the public API. See
createPollWorkflowTaskQueueResponse.
The essential start-before-delivery invariant is preserved. For a closer service boundary,
Matching should return the task plus the data obtained from History's start operation, with
Frontend retaining public wire encoding. Returning only a QueueTask obscures that contract.
Structure and persistence conclusions
cmd/server,temporal,service/{frontend,history,matching,worker}, andcommon/persistencefollow the useful upstream boundaries. Keeping service interfaces with their Effect layers is a reasonable TypeScript adaptation of Go interfaces and Fx modules.common/src/state.tsconsolidates concepts spread upstream acrossapi/persistence,api/taskqueue,api/token,common/tqid, and History workflow state. It is a shared teaching vocabulary, not a wire protocol package or a literal upstream directory equivalent.- Matching correctly keeps durable backlog/range/ack metadata separate from loaded managers and
waiting polls. Its scalar acknowledgement model corresponds to the FIFO-style
ackManager, not the complete priority/fairness backlog implementations. Per-task checkpoint/delete and consecutive task IDs simplify upstream checkpointing, garbage collection, and range allocation. - History owns execution transitions and generated transfer/timer/visibility work. Visibility is a storage projection, and SDK workflow execution remains outside the server. These are useful, preserved ownership rules.
- JSONB events,
history_partition, and separate toy task tables are intentionally inspectable representations. They do not reproduce upstream history branches, serialized mutable-state maps,history_immediate_tasks/history_scheduled_tasks, or all Matching v2 tables. - The internal Worker is a scanner sketch. CHASM migration, scaling, and fairness exercises must be read with their explicit limits; they are not evidence of corresponding production systems.
Next alignment work: refine the Matching/History poll result so History returns the state used for delivery as part of recording the start. Internal RPC deployment and real shard ownership are a larger, separate step.
Verification of these corrections
On 2026-10-03, pnpm check passed TypeScript, 83 tests, and the stock SDK
retry/timer/signal/result and worker-crash integration. pnpm test:postgres passed
the migration, concurrent-write, restart-routing, and process-crash checks described above.
A separate stock SDK run against tiny_temporal completed official-greeting-1791082108880
(run-43). SQL inspection confirmed migration 004, 19 saved events, result
Hello Ada: approved, completed visibility, and no remaining Matching or transfer tasks
for that run. These checks cover the implemented subset, not upstream conformance.
Trace one activity
sequenceDiagram
participant SDK as SDK Worker
participant F as Frontend
participant M as Matching
participant H as History
participant Q as History queue processor
SDK->>F: PollWorkflowTaskQueue
F->>M: PollWorkflowTaskQueue
M->>H: RecordWorkflowTaskStarted
M-->>F: task + history
F-->>SDK: task + history
SDK->>F: RespondWorkflowTaskCompleted(schedule activity)
F->>H: RespondWorkflowTaskCompleted
H->>Q: transfer task
Q->>M: AddActivityTask
SDK->>F: PollActivityTaskQueue
F->>M: PollActivityTaskQueue
M->>H: RecordActivityTaskStarted
M-->>F: activity task
F-->>SDK: activity task
SDK->>F: RespondActivityTaskCompleted
F->>H: RespondActivityTaskCompleted
This diagram shows the upstream response contract, which the toy now follows: every arrow between server roles is a generated historyservice or matchingservice RPC, and Matching returns the task with the data History supplied when it recorded the start. Compare the pinned OSS workflow lifecycle diagrams with docs/walkthrough.md, or run pnpm wire to print the calls.
Where the mapping breaks
- Storage shape:
Databasestill puts executions, embedded event arrays, History tasks, shard leases, Matching backlog, user data, and visibility in one object. The per-service stores (ExecutionStore,TaskStore,VisibilityStore,NamespaceStore) make ownership a capability each service must be granted, without requiring separate physical databases. The PostgreSQL adapter separates records into tables, and still takes one global lock in addition to the shard row it fences. OSS hasExecutionManagerandTaskManagerand distinct SQL rows for executions, history, and queues. See the pinned SQL schema. - Identity and placement: History shards and Matching partitions have owners, leases, and fencing, but membership is a static list hashed by modulo. Only the root partition serves ordinary worker calls unless the Matching client is given more partitions; the forwarding tree is a fixed binary tree. See Matching architecture.
- Reliability protocol: History atomically commits state and intents under a shard lease. Matching records starts before delivery; History timeouts recover lost worker attempts. Polls cancel across every RPC hop and task tokens identify attempts. There is no replication, retry backoff, or host failure detection. Activities can execute more than once.
- Workflow semantics: The generator SDK replays public history one command at a time and interprets signals locally. The optional stock SDK brings its real sandbox and Core runtime through a bounded protocol subset. Updates, child workflows, cancellation, replication, and CHASM runtime remain outside the model.
- Source structure:
cmd/serverconfigures startup,temporalcomposes one layer graph per host, and eachservice/*/src/service.tsowns that role's listener and loops.api/holds generated contracts;proto/owns the generator;client/holds the routed clients. In this model, role packages never import each other andcommoncannot import services or clients (stricter than upstream’s shared Go helper/task packages), and generated code depends only on Effect and local API declarations. Private pnpm workspaces, explicit exports, project references, and dependency-cruiser enforce these boundaries, including type-only imports.
The polling and lifecycle walkthrough shows what happens to waiting polls and in-flight activities when a partition unloads. Each host's Effect scope serves the lifecycle role of upstream service Start/Stop hooks.
For a Matching-to-Walker migration, the toy is most useful for identifying the contract to preserve: History sends tasks; Matching owns task-queue delivery; Frontend routes worker polls; Matching calls History to record starts; Matching's durable backlog and metadata live behind persistence interfaces. It cannot tell you which Walker processes, storage APIs, rollout controls, or hosted-only behaviors to change. Those require reading the Walker and SaaS code directly.
Read the Matching lifecycle lab for current OSS boundaries and a separately labeled exercise based on the proposed CHASM user-data migration.
Review against the upstream architecture assets
Reviewed on 2026-10-05 against the local temporal/architecture-review directory, its generator sources, and the referenced Go code. The core service responsibilities and ordinary workflow-task path align; the import graph and persistence layout are not literal replicas. The generated import inventory reports 159 OSS packages, 9 SaaS packages, and 62 direct internal Matching imports. These are selected packages from that checkout, not the complete Temporal repository or the toy's pinned protobuf revision.
What each asset establishes
| Assets reviewed (DOT sources and corresponding SVG/PNG renderings) | Comparison with this repository |
|---|---|
repo-layout.* |
The api, client, common, service, schema, and composition responsibilities correspond. Upstream OSS has one Go module with many packages; our private workspaces are coarser TypeScript boundaries. Package-local src/ is a TypeScript convention. |
oss-subsystem-imports.*, oss-matching-direct-imports.*, generation-stats.json, generate.py |
These describe imports, not RPC calls. The aggregate view filters to an explicit subsystem allowlist. The source query includes descendant packages such as client/history/historytest; that test helper contributes imports of History implementation packages. Aggregating directories can produce apparent cycles without any Go package cycle. Our graph should not reproduce those edges blindly. |
runtime-boundaries.*, workflow-task-flow.*, worker-poll-flow.* |
Frontend routes starts/completions to History and polls to Matching. History persists state plus transfer intent, then its processor sends Add*Task. Matching sync-matches or persists backlog, calls History to record a start, and returns the poll response. Our code follows that core path. These diagrams are curated in generate.py; only the import diagrams are mechanically extracted. |
matching-fx-registration.md |
Provider and lifecycle registration inventory, not a resolved dependency graph. Our per-host layers in temporal/src/fx.ts and scoped service loops model injection and lifetime, but not every upstream provider, interceptor, or worker-deployment workflow. |
zinnia-poll-trace.json, zinnia-poll-sequence.md, zinnia-poll-trace.*, render_live_trace.py |
Ten observed spans show Frontend → Matching → Matching → History. The History calls in this particular trace are GetMutableState and GetWorkflowExecutionHistory, not RecordWorkflowTaskStarted. The toy's normal poll instead receives history in the start response. The trace proves neither separate Matching hosts nor the database driver's call. The referenced timeline HTML and its rendering template are absent from the supplied directory. |
db-inspection/README.md, relationships.md, queries.sql, install_views.sql, decode_db_blob.go |
History owns execution/current-run/history records; Matching owns queue metadata, delivery rows, and user data. Both may share a core database. This ownership corresponds to our stores, but the upstream protobuf blobs, history branches/batches, physical keys, and absence of foreign keys do not match our JSONB schema. The SQL/decoder assets target the upstream database and cannot be reused against this toy. No database inspection/install scripts were executed for this review. |
saas-storage-imports.*, saas-matching-storage-gap.* |
The captured SaaS path selects Cassandra Matching stores; Walker Matching stores are explicitly unimplemented/proposed in these assets. This supports keeping the persistence implementation behind an interface, not adding Walker or moving the Matching process. The toy implements memory and PostgreSQL only; no current SaaS deployment claim follows from this snapshot. |
durable-execution-comparison.md |
Useful conceptual context for database-backed transactions, durable task intents, worker polling, and asynchronous visibility. It is not a generated import or execution graph. Its wider Restate/Effect feature comparisons are outside this alignment review. |
Actual runtime correspondence
This is a source-reviewed responsibility diagram, not generated from imports and not a claim that every edge appeared in one trace:
flowchart LR
SDK[External SDK client / worker] -->|public gRPC| F[Frontend]
F -->|start, complete, signal, read| H[History]
F -->|long poll| M[Matching]
H -->|transfer processor: AddTask| M
M -->|RecordTaskStarted| H
M -->|partition forwarding| M
H -->|state, events, intents; shard fence| ES[ExecutionStore]
M -->|backlog, metadata, user data; queue fence| TS[TaskStore]
H -->|asynchronous projection| V[Visibility]
F -->|list/read| V
V --> VS[VisibilityStore]
Internal service arrows use generated contracts over Effect RPC here; upstream uses gRPC/protobuf. The embedded teaching SDK calls Frontend through an in-process RPC client; the stock SDK uses public gRPC. The internal Worker is a separate maintenance scanner, not the application worker that polls Frontend.
Evidence in the toy: Frontend handler, History transaction engine, transfer processor, Matching dispatch and forwarding, backlog manager, History start response, and composition. pnpm wire was run during this review and observed start → add workflow task → record workflow-task start → completion → add activity task → record activity-task start → completion. The example filters empty polls and history-result polling, so its output is not a complete distributed trace. Dispatch tests cover rejected transfers and retryable task-start failures.
Corrections and remaining limits
- Our import policy is stricter than upstream. Upstream Matching imports
service/history/api,service/history/hsm/nexusoperations, andservice/worker/workerdeployment. Upstream persistence importsservice/history/tasks. These are shared code/type dependencies, not in-process calls into another running service. Conversely, Matching receivesresource.HistoryClientby injection, so its runtime History calls do not require a direct import ofclient/history. A source graph is not the service call graph. Our rule remains useful for the smaller model, but must not be described as upstream's exact directory rule. - Store ownership has explicit import boundaries. History imports ExecutionStore; Matching imports TaskStore; Frontend uses the visibility manager and namespace registry; the scanner reads several stores. Separate public entrypoints and dependency-cruiser allowlists enforce these imports, including type-only imports. Process composition still supplies a shared persistence layer. This policy is stronger than upstream: Go exports the interfaces from one persistence package and uses typed constructor dependencies, without equivalent per-role visibility restrictions. The import graph does not prove runtime isolation.
- Storage preserves intent, not physical design or scale. The toy has JSONB records, individual history-event rows, a run-ID-centered schema, two execution foreign keys, and a global History transaction lock in addition to shard fencing. Upstream uses protobuf state, history trees/batches, composite identities, and conditional writes without those foreign keys. Our schema cannot establish upstream throughput, retention, branch behavior, or SQL compatibility.
- The runtime subset is intentionally smaller. No full worker-deployment control workflows, Nexus/HTTP surface, replication, CHASM runtime, production membership, or complete history-fetch/sticky-cache behavior is implied. The sampled Zinnia poll and the curated ordinary-task diagram describe different paths; neither alone specifies all required behavior.
The review found no reversed ownership or direct cross-service engine call in the implemented task path. It does not establish full Temporal or SaaS equivalence. pnpm deps:graph now generates a side-by-side comparison at three altitudes, pnpm deps:matching opens with Matching's direct neighborhood selected, and pnpm deps:mermaid retains the compact Markdown diagram. All are source-import views; pnpm wire provides an observed execution. Neither substitutes for the other.