typeclaw 0.36.8 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -2
  3. package/src/agent/index.ts +31 -11
  4. package/src/agent/live-sessions.ts +12 -0
  5. package/src/agent/model-fallback.ts +17 -15
  6. package/src/agent/model-overrides.ts +2 -2
  7. package/src/agent/session-meta.ts +10 -0
  8. package/src/agent/subagents.ts +11 -2
  9. package/src/agent/system-prompt.ts +9 -3
  10. package/src/agent/todo/continuation-policy.ts +6 -3
  11. package/src/agent/todo/continuation-wiring.ts +4 -2
  12. package/src/agent/todo/continuation.ts +3 -3
  13. package/src/agent/tools/todo/index.ts +27 -4
  14. package/src/bundled-plugins/agent-browser/index.ts +33 -108
  15. package/src/bundled-plugins/agent-browser/shim.ts +3 -94
  16. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
  17. package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
  18. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
  19. package/src/bundled-plugins/memory/README.md +80 -23
  20. package/src/bundled-plugins/memory/append-tool.ts +74 -53
  21. package/src/bundled-plugins/memory/citation-superset.ts +4 -0
  22. package/src/bundled-plugins/memory/citations.ts +54 -0
  23. package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
  24. package/src/bundled-plugins/memory/dreaming.ts +444 -21
  25. package/src/bundled-plugins/memory/index.ts +544 -400
  26. package/src/bundled-plugins/memory/load-memory.ts +87 -10
  27. package/src/bundled-plugins/memory/load-shards.ts +48 -22
  28. package/src/bundled-plugins/memory/memory-logger.ts +95 -106
  29. package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
  30. package/src/bundled-plugins/memory/parent-link.ts +33 -0
  31. package/src/bundled-plugins/memory/paths.ts +12 -0
  32. package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
  33. package/src/bundled-plugins/memory/references/load-references.ts +212 -0
  34. package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
  35. package/src/bundled-plugins/memory/search-tool.ts +282 -45
  36. package/src/bundled-plugins/memory/stream-events.ts +1 -0
  37. package/src/bundled-plugins/memory/stream-io.ts +28 -3
  38. package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
  39. package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
  40. package/src/bundled-plugins/memory/vector/config.ts +28 -0
  41. package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
  42. package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
  43. package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
  44. package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
  45. package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
  46. package/src/bundled-plugins/memory/vector/passages.ts +125 -0
  47. package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
  48. package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
  49. package/src/bundled-plugins/memory/vector/startup.ts +71 -0
  50. package/src/bundled-plugins/memory/vector/store.ts +203 -0
  51. package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
  52. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
  53. package/src/channels/router.ts +239 -40
  54. package/src/cli/incomplete-init.ts +57 -0
  55. package/src/cli/init.ts +143 -12
  56. package/src/cli/inspect.ts +11 -5
  57. package/src/cli/model.ts +112 -34
  58. package/src/cli/restart.ts +24 -0
  59. package/src/cli/start.ts +24 -0
  60. package/src/cli/tunnel.ts +53 -8
  61. package/src/config/config.ts +110 -19
  62. package/src/config/index.ts +5 -1
  63. package/src/config/models-mutation.ts +29 -11
  64. package/src/config/providers-mutation.ts +2 -2
  65. package/src/config/providers.ts +146 -12
  66. package/src/container/shared.ts +9 -0
  67. package/src/container/start.ts +87 -4
  68. package/src/cron/consumer.ts +13 -7
  69. package/src/hostd/models.ts +64 -0
  70. package/src/hostd/paths.ts +6 -0
  71. package/src/hostd/portbroker-manager.ts +2 -2
  72. package/src/init/checkpoint.ts +201 -0
  73. package/src/init/dockerfile.ts +121 -34
  74. package/src/init/gitignore.ts +7 -7
  75. package/src/init/index.ts +41 -9
  76. package/src/init/models-dev.ts +96 -21
  77. package/src/init/oauth-login.ts +3 -3
  78. package/src/init/progress.ts +29 -0
  79. package/src/init/validate-api-key.ts +4 -0
  80. package/src/inspect/index.ts +13 -6
  81. package/src/inspect/item-list.ts +11 -2
  82. package/src/inspect/live-list.ts +65 -0
  83. package/src/inspect/open-item.ts +22 -1
  84. package/src/inspect/session-list.ts +29 -0
  85. package/src/models/embedding-model.ts +114 -0
  86. package/src/models/transformers-version.ts +55 -0
  87. package/src/plugin/types.ts +3 -0
  88. package/src/portbroker/container-server.ts +23 -0
  89. package/src/portbroker/forward-request-bus.ts +35 -0
  90. package/src/portbroker/forward-result-bus.ts +2 -3
  91. package/src/portbroker/hostd-client.ts +182 -36
  92. package/src/portbroker/index.ts +6 -1
  93. package/src/portbroker/protocol.ts +9 -2
  94. package/src/run/channel-session-factory.ts +11 -1
  95. package/src/run/index.ts +41 -7
  96. package/src/server/command-runner.ts +24 -1
  97. package/src/server/index.ts +42 -8
  98. package/src/shared/index.ts +2 -0
  99. package/src/shared/protocol.ts +31 -0
  100. package/src/skills/typeclaw-channels/SKILL.md +4 -4
  101. package/src/skills/typeclaw-config/SKILL.md +2 -2
  102. package/src/skills/typeclaw-memory/SKILL.md +3 -1
  103. package/src/skills/typeclaw-permissions/SKILL.md +3 -3
  104. package/src/skills/typeclaw-skills/SKILL.md +1 -1
  105. package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
  106. package/src/tunnels/providers/cloudflare-quick.ts +65 -7
  107. package/src/tunnels/upstream-probe.ts +25 -0
  108. package/typeclaw.schema.json +156 -67
  109. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
  110. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
  111. package/src/portbroker/bind-with-forward.ts +0 -102
package/README.md CHANGED
@@ -31,7 +31,7 @@ If you're like me, TypeClaw is the right choice. If not, that's fine too.
31
31
 
32
32
  - 🐳 **Sandboxed by default** — every agent runs in its own Docker container with `.env` injection and bind-mounted host folders
33
33
  - 🔌 **Plugin system** — plain TypeScript modules contribute tools, skills, subagents, channels, commands, and typed config
34
- - 💬 **Multi-channel** — Slack, Discord, Telegram, KakaoTalk, GitHub webhooks, and a websocket TUI; one agent, many inboxes
34
+ - 💬 **Multi-channel** — Slack, Discord, Telegram, LINE, KakaoTalk, GitHub webhooks, and a websocket TUI; one agent, many inboxes
35
35
  - ⏰ **Cron** — schedule prompts or shell commands; per-job coalescing so slow jobs don't pile up
36
36
  - 📚 **Skills on demand** — markdown procedures the agent loads only when relevant; zero token cost until used
37
37
  - 🔎 **Web research** — bundled `scout` subagent plus first-class `web_search` and `web_fetch` tools (DuckDuckGo via curl-impersonate, Wikipedia)
@@ -97,7 +97,7 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the recommended local dev loop (`bu
97
97
 
98
98
  ## Acknowledgments
99
99
 
100
- - **Multi-channel** is powered by [agent-messenger](https://github.com/agent-messenger/agent-messenger) — every non-GitHub adapter (`slack-bot`, `discord-bot`, `telegram-bot`, `kakaotalk`) is built on its SDK. Thanks to the maintainers for the credential extraction, listener protocols, and platform coverage that made multi-channel a feature instead of a year-long project.
100
+ - **Multi-channel** is powered by [agent-messenger](https://github.com/agent-messenger/agent-messenger) — every non-GitHub adapter (`slack-bot`, `discord-bot`, `telegram-bot`, `line`, `kakaotalk`) is built on its SDK. Thanks to the maintainers for the credential extraction, listener protocols, and platform coverage that made multi-channel a feature instead of a year-long project.
101
101
  - **Subagent architecture** is inspired by [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent) by [@code-yeongyu](https://github.com/code-yeongyu). Thanks for the shape that made this clean.
102
102
 
103
103
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.36.8",
3
+ "version": "0.37.0",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -44,6 +44,7 @@
44
44
  "dependencies": {
45
45
  "@clack/core": "^1.2.0",
46
46
  "@clack/prompts": "^1.2.0",
47
+ "@huggingface/transformers": "4.2.0",
47
48
  "@mariozechner/pi-coding-agent": "^0.67.3",
48
49
  "@mariozechner/pi-tui": "^0.67.3",
49
50
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -54,6 +55,7 @@
54
55
  "cron-parser": "^5.5.0",
55
56
  "jq-wasm": "^1.1.0-jq-1.8.1",
56
57
  "jsdom": "^29.0.2",
58
+ "proper-lockfile": "^4.1.2",
57
59
  "qrcode": "^1.5.4",
58
60
  "turndown": "^7.2.4",
59
61
  "zod": "^4.3.6"
@@ -66,7 +68,6 @@
66
68
  "@types/qrcode": "^1.5.6",
67
69
  "@types/sinonjs__fake-timers": "^15.0.1",
68
70
  "@types/turndown": "^5.0.6",
69
- "@types/ws": "^8.18.1",
70
71
  "@typescript/native-preview": "^7.0.0-dev.20260416.1",
71
72
  "oxfmt": "^0.45.0",
72
73
  "oxlint": "^1.60.0"
@@ -15,7 +15,7 @@ import { loadMemory } from '@/bundled-plugins/memory/load-memory'
15
15
  import type { ChannelRouter } from '@/channels/router'
16
16
  import type { ReactionRef } from '@/channels/types'
17
17
  import { getConfig, resolveModel, resolveProfile } from '@/config'
18
- import { defaultThinkingLevelForRef, providerForModelRef, type KnownModelRef } from '@/config/providers'
18
+ import { defaultThinkingLevelForRef, providerForModelRef, type ModelRef } from '@/config/providers'
19
19
  import { renderMcpCatalog } from '@/mcp/catalog'
20
20
  import type { McpManager } from '@/mcp/manager'
21
21
  import { createMcpDispatcherTools, MCP_DISPATCHER_TOOL_NAMES } from '@/mcp/tools'
@@ -196,7 +196,7 @@ export type CreateSessionOptions = {
196
196
  // pinned to the next ref in the chain after the previous one failed. When
197
197
  // set, `profile` is still recorded for the fallback-warning bookkeeping;
198
198
  // the profile→refs resolution is skipped.
199
- refOverride?: KnownModelRef
199
+ refOverride?: ModelRef
200
200
  // Defensive ceiling on cumulative bytes of tool-result text per session,
201
201
  // applied to the named tools only. See `src/agent/tool-result-budget.ts`
202
202
  // for the rationale. Intended for subagents that read large files
@@ -221,6 +221,12 @@ export type CreateSessionOptions = {
221
221
  subagentRegistry?: SubagentRegistry
222
222
  createSessionForSubagent?: CreateSessionForSubagent
223
223
  allowBackgroundFromSubagent?: boolean
224
+ // When true, the `# Memory` section is omitted from the system prompt and
225
+ // long-term memory is injected per-turn into the user prompt instead (the
226
+ // memory plugin's vector `session.turn.start` path). Derived once at boot
227
+ // from `memory.vector.enabled`, which is restart-required — so the boot
228
+ // snapshot stays coherent with the per-turn injection decision.
229
+ suppressSystemMemory?: boolean
224
230
  }
225
231
 
226
232
  export type CreateSessionResult = {
@@ -241,7 +247,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
241
247
  // exactly what they're doing.
242
248
  // `refOverride` lets the model-fallback helper pin a specific entry from
243
249
  // the chain when it recreates a session after the previous ref failed.
244
- const activeRef: KnownModelRef = options.refOverride ?? resolved.ref
250
+ const activeRef: ModelRef = options.refOverride ?? resolved.ref
245
251
  const { authStorage, modelRegistry } = getAuthFor(providerForModelRef(activeRef))
246
252
 
247
253
  const materializedSkills =
@@ -270,6 +276,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
270
276
  ...(options.runtimeVersion !== undefined ? { runtimeVersion: options.runtimeVersion } : {}),
271
277
  ...(options.mcpManager !== undefined ? { mcpManager: options.mcpManager } : {}),
272
278
  ...(options.subagentRegistry !== undefined ? { subagentRegistry: options.subagentRegistry } : {}),
279
+ ...(options.suppressSystemMemory !== undefined ? { suppressSystemMemory: options.suppressSystemMemory } : {}),
273
280
  })
274
281
 
275
282
  const getOrigin: () => SessionOrigin | undefined =
@@ -944,6 +951,12 @@ export type CreateResourceLoaderOptions = {
944
951
  // 'full' to force the heavy prompt even on an unattended origin (rarely
945
952
  // useful; mostly an escape hatch for ad-hoc debugging).
946
953
  mode?: SystemPromptMode
954
+ // When true, the `# Memory` section is omitted from the system prompt and
955
+ // long-term memory is injected per-turn into the user prompt instead (the
956
+ // memory plugin's vector `session.turn.start` path). Derived once at boot
957
+ // from `memory.vector.enabled` — vector is restart-required, so the boot
958
+ // snapshot is coherent with the per-turn injection decision.
959
+ suppressSystemMemory?: boolean
947
960
  }
948
961
 
949
962
  // Origins where the operator-facing DEFAULT_SYSTEM_PROMPT, git-nudge, and the
@@ -1024,8 +1037,8 @@ export type SystemPromptComposition = {
1024
1037
  // memory/ after every turn, so the dirty-files list is empty most of
1025
1038
  // the time.
1026
1039
  // 3. memorySection — volatile: MEMORY.md grows on every dream cycle and
1027
- // memory/yyyy-MM-dd.md grows after every channel turn that triggers
1028
- // memory-logger.
1040
+ // memory/streams/yyyy-MM-dd.jsonl grows after every channel turn that
1041
+ // triggers memory-logger.
1029
1042
  //
1030
1043
  // The wall-clock anchor that used to live here as `## Now` moved out
1031
1044
  // entirely. It is now injected into the user turn at each `session.prompt`
@@ -1099,12 +1112,19 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
1099
1112
  // gather point.
1100
1113
  const selfPromise = loadSelf(agentDir)
1101
1114
  const gitNudgeSettled = mode === 'slim' ? Promise.resolve(ok('')) : settle(renderGitNudge(agentDir))
1102
- const memorySettled = settle(
1103
- loadMemory(agentDir, {
1104
- ...(options.origin !== undefined ? { origin: options.origin } : {}),
1105
- ...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
1106
- }),
1107
- )
1115
+ // Vector agents omit the `# Memory` section entirely: long-term memory is
1116
+ // injected per-turn into the user prompt by the memory plugin's vector
1117
+ // `session.turn.start` hook. Keeping both would double-inject and re-break the
1118
+ // cache prefix this change exists to protect the invariant is
1119
+ // `suppressSystemMemory === memory.vector.enabled`.
1120
+ const memorySettled = options.suppressSystemMemory
1121
+ ? Promise.resolve(ok(''))
1122
+ : settle(
1123
+ loadMemory(agentDir, {
1124
+ ...(options.origin !== undefined ? { origin: options.origin } : {}),
1125
+ ...(options.plugins?.sessionId !== undefined ? { currentSessionId: options.plugins.sessionId } : {}),
1126
+ }),
1127
+ )
1108
1128
 
1109
1129
  let self = await selfPromise
1110
1130
 
@@ -1,8 +1,16 @@
1
1
  import type { AgentSession } from './index'
2
+ import type { MinimalSessionOrigin } from './session-meta'
2
3
 
3
4
  export type LiveAgentSession = {
4
5
  sessionId: string
5
6
  session: Pick<AgentSession, 'subscribe'>
7
+ // Surfaced by the inspect picker for sessions not yet on disk: pi-coding-agent
8
+ // defers the first .jsonl write until the first assistant message, so without
9
+ // these a mid-reply session is invisible. Optional so subscribe-only test
10
+ // harnesses can still register `{ sessionId, session }`; live-listing skips
11
+ // entries lacking an origin.
12
+ origin?: MinimalSessionOrigin
13
+ registeredAtMs?: number
6
14
  }
7
15
 
8
16
  export class LiveSessionRegistry {
@@ -24,6 +32,10 @@ export class LiveSessionRegistry {
24
32
  return this.entries.has(sessionId)
25
33
  }
26
34
 
35
+ listLive(): LiveAgentSession[] {
36
+ return [...this.entries.values()].filter((e) => e.origin !== undefined)
37
+ }
38
+
27
39
  size(): number {
28
40
  return this.entries.size
29
41
  }
@@ -1,6 +1,6 @@
1
1
  import { resolveProfile } from '@/config'
2
2
  import type { Models } from '@/config/config'
3
- import type { KnownModelRef } from '@/config/providers'
3
+ import type { KnownModelRef, ModelRef } from '@/config/providers'
4
4
 
5
5
  import type { AgentSession } from './index'
6
6
  import { subscribeProviderErrors } from './provider-error'
@@ -15,18 +15,20 @@ import { renderTurnTimeAnchor } from './system-prompt'
15
15
  // the final entry, on full-chain failure). Callers that need to keep using
16
16
  // the session for subsequent turns store these in their state; callers that
17
17
  // tear down per-turn (cron) just call `dispose()` and discard.
18
- export type FallbackPromptResult = {
18
+ type FallbackModelRef = KnownModelRef | ModelRef
19
+
20
+ export type FallbackPromptResult<TRef extends FallbackModelRef = ModelRef> = {
19
21
  success: boolean
20
- refUsed: KnownModelRef
21
- attempts: FallbackAttempt[]
22
+ refUsed: TRef
23
+ attempts: FallbackAttempt<TRef>[]
22
24
  session: AgentSession
23
25
  dispose: () => Promise<void>
24
26
  // When `success === false`, this is the error from the final attempt.
25
27
  lastError?: Error
26
28
  }
27
29
 
28
- export type FallbackAttempt = {
29
- ref: KnownModelRef
30
+ export type FallbackAttempt<TRef extends FallbackModelRef = ModelRef> = {
31
+ ref: TRef
30
32
  // 'hard' = session.prompt() threw. 'soft' = pi-coding-agent surfaced an
31
33
  // upstream error via stopReason: 'error' on the final assistant message.
32
34
  // 'success' = the turn finished cleanly.
@@ -40,7 +42,7 @@ export type FallbackAttempt = {
40
42
  //
41
43
  // Exported so callers can introspect the chain (e.g. logs, telemetry) before
42
44
  // firing the prompt — useful for `[cron] ${jobId}: trying chain a → b → c`.
43
- export function resolveFallbackChain(models: Models, profile: string | undefined): KnownModelRef[] {
45
+ export function resolveFallbackChain(models: Models, profile: string | undefined): ModelRef[] {
44
46
  return resolveProfile(models, profile).refs
45
47
  }
46
48
 
@@ -62,18 +64,18 @@ export function resolveFallbackChain(models: Models, profile: string | undefined
62
64
  // (console.error in the server drain, channel reaction in the router,
63
65
  // cron-job status). This keeps the helper composable with the existing
64
66
  // error-handling code at each call site.
65
- export async function promptWithFallback(opts: {
66
- refs: KnownModelRef[]
67
+ export async function promptWithFallback<TRef extends FallbackModelRef>(opts: {
68
+ refs: TRef[]
67
69
  text: string
68
- createSessionForRef: (ref: KnownModelRef) => Promise<{ session: AgentSession; dispose: () => Promise<void> }>
70
+ createSessionForRef: (ref: TRef) => Promise<{ session: AgentSession; dispose: () => Promise<void> }>
69
71
  // Called after each non-final attempt so callers can log the per-attempt
70
72
  // failure with their own context (sessionId, channel key, job id, ...).
71
- onAttemptFailed?: (attempt: FallbackAttempt) => void
72
- }): Promise<FallbackPromptResult> {
73
+ onAttemptFailed?: (attempt: FallbackAttempt<TRef>) => void
74
+ }): Promise<FallbackPromptResult<TRef>> {
73
75
  if (opts.refs.length === 0) {
74
76
  throw new Error('promptWithFallback: refs[] must be non-empty')
75
77
  }
76
- const attempts: FallbackAttempt[] = []
78
+ const attempts: FallbackAttempt<TRef>[] = []
77
79
  let lastError: Error | undefined
78
80
  for (let i = 0; i < opts.refs.length; i++) {
79
81
  const ref = opts.refs[i]!
@@ -92,7 +94,7 @@ export async function promptWithFallback(opts: {
92
94
  await session.prompt(`${renderTurnTimeAnchor()}\n\n${opts.text}`)
93
95
  } catch (err) {
94
96
  const error = err instanceof Error ? err : new Error(String(err))
95
- const attempt: FallbackAttempt = { ref, outcome: 'hard', errorMessage: error.message }
97
+ const attempt: FallbackAttempt<TRef> = { ref, outcome: 'hard', errorMessage: error.message }
96
98
  attempts.push(attempt)
97
99
  lastError = error
98
100
  if (!isLast) opts.onAttemptFailed?.(attempt)
@@ -104,7 +106,7 @@ export async function promptWithFallback(opts: {
104
106
  continue
105
107
  }
106
108
  if (softError !== undefined) {
107
- const attempt: FallbackAttempt = { ref, outcome: 'soft', errorMessage: softError.message }
109
+ const attempt: FallbackAttempt<TRef> = { ref, outcome: 'soft', errorMessage: softError.message }
108
110
  attempts.push(attempt)
109
111
  lastError = softError
110
112
  if (!isLast) opts.onAttemptFailed?.(attempt)
@@ -1,6 +1,6 @@
1
1
  import type { Api, Model } from '@mariozechner/pi-ai'
2
2
 
3
- import { providerForModelRef, type KnownModelRef, type KnownProviderId } from '@/config/providers'
3
+ import { providerForModelRef, type KnownModelRef, type KnownProviderId, type ModelRef } from '@/config/providers'
4
4
 
5
5
  // Providers whose base URL can be swapped to an upstream-compatible gateway at
6
6
  // runtime. Each env var mirrors the upstream SDK's own name so a credential /
@@ -26,7 +26,7 @@ type OverridableProviderId = keyof typeof PROVIDER_BASE_URL_ENV
26
26
  // data that must never be mutated.
27
27
  export function applyModelRuntimeOverrides<TApi extends Api>(
28
28
  model: Model<TApi>,
29
- ref: KnownModelRef,
29
+ ref: KnownModelRef | ModelRef | string,
30
30
  env: NodeJS.ProcessEnv = process.env,
31
31
  ): Model<TApi> {
32
32
  const providerId = providerForModelRef(ref)
@@ -1,5 +1,15 @@
1
+ import type { LiveSessionOriginPayload } from '@/shared'
2
+
1
3
  import type { SessionOrigin } from './session-origin'
2
4
 
5
+ // Bidirectional structural equality with the wire mirror in @/shared/protocol.
6
+ // @/shared cannot import this module (it is a leaf), so the type cannot be
7
+ // shared directly; these assignments fail typecheck if either side drifts.
8
+ const _originIsWireCompatible: LiveSessionOriginPayload = null as unknown as MinimalSessionOrigin
9
+ const _wireIsOriginCompatible: MinimalSessionOrigin = null as unknown as LiveSessionOriginPayload
10
+ void _originIsWireCompatible
11
+ void _wireIsOriginCompatible
12
+
3
13
  export const SESSION_META_CUSTOM_TYPE = 'typeclaw.session-meta'
4
14
 
5
15
  export type SessionMetaPayload = {
@@ -262,15 +262,24 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
262
262
  ? { sessionId, agentDir, ...(origin !== undefined ? { origin } : {}) }
263
263
  : undefined
264
264
  const userPromptForTurn = override?.userPrompt ?? options.userPrompt
265
+ // Per-turn memory injection for vector agents: subagents have no
266
+ // system-prompt `# Memory` section (their prompt is a systemPromptOverride),
267
+ // so the turn-start hook renders memory into `retrievalContext.results`,
268
+ // appended to the user turn below. Empty for non-vector agents.
269
+ const retrievalContext = { results: '' }
265
270
  try {
266
271
  if (hooks && turnEvent !== undefined) {
267
- await hooks.runSessionTurnStart({ ...turnEvent, userPrompt: userPromptForTurn })
272
+ await hooks.runSessionTurnStart({ ...turnEvent, userPrompt: userPromptForTurn, retrievalContext })
268
273
  }
269
274
  if (backgroundDrain !== undefined) {
270
275
  drainWatch = beginSubagentDrainWatch(backgroundDrain)
271
276
  }
272
277
  try {
273
- await session.prompt(`${renderTurnTimeAnchor()}\n\n${userPromptForTurn}`)
278
+ const turnText =
279
+ retrievalContext.results.length > 0
280
+ ? `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}\n\n${retrievalContext.results}`
281
+ : `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}`
282
+ await session.prompt(turnText)
274
283
  } finally {
275
284
  if (hooks && turnEvent !== undefined) {
276
285
  await hooks.runSessionTurnEnd(turnEvent)
@@ -56,7 +56,7 @@ When the user gives you work, start doing it in the same turn — a real action,
56
56
 
57
57
  ## Tracking your work
58
58
 
59
- For any multi-step or long-running task, maintain a todo list with \`todo_write\` and mark items complete as you finish them. This is not bookkeeping for its own sake: if this session is interrupted — a restart, a crash, or simply a later turn — the runtime uses the remaining incomplete items to resume the work instead of silently dropping it. Write the list when you start the work, update statuses as you go, and call \`todo_clear\` when everything is genuinely done. A single-step request needs no todo list.
59
+ For any multi-step or long-running task, maintain a todo list with \`todo_write\` and mark items complete as you finish them. This is not bookkeeping for its own sake: if this session is interrupted — a restart, a crash, or simply a later turn — the runtime uses the remaining incomplete items to resume the work instead of silently dropping it. Write the list when you start the work and update statuses as you go; once your \`todo_write\` leaves no incomplete items, the runtime clears the list for you. Use \`todo_clear\` only to abandon a task with items still incomplete. A single-step request needs no todo list.
60
60
 
61
61
  ## Tool-call style
62
62
 
@@ -81,13 +81,17 @@ Use this only when the work belongs in *your* session. For self-contained long w
81
81
 
82
82
  ## Version control
83
83
 
84
- Your agent folder is a git repository.
84
+ Your agent folder is a git repository, but **it is your own private backup repo — not a software project you develop.** It exists so TypeClaw can snapshot your identity files, \`sessions/\`, and \`memory/\` over time. It has no GitHub remote, nothing is pushed anywhere, and it is **not** a checkout of any project's source code. So when you commit here, you are saving your own state — not contributing to a codebase.
85
+
86
+ This matters when the user asks you to work on an actual software project — fix a bug, build a feature, open a pull request. **That work does not happen in your agent folder.** Clone the project's repo somewhere else first (e.g. \`/tmp/<repo>\`), do the work there, and open the PR from that clone with \`gh\`. Never \`git init\`, add a remote, or try to push your agent folder as if it were the project — and if you can't find the project repo or its remote, ask the user where it lives instead of treating this folder as the project. The two are separate: this folder is *where you live*, the project clone is *where you work*.
87
+
88
+ Commits to your agent folder (your own state):
85
89
 
86
90
  - Commit any files you created, edited, or deleted before declaring a task done. One logical change = one commit; split unrelated changes.
87
91
  - Use \`git add <paths>\` (not \`git add -A\`). Imperative commit messages ("Update SOUL.md to be less formal"); explain *why* in the body if non-obvious.
88
92
  - Never commit \`secrets.json\`, \`.env\`, or anything under \`workspace/\` — truly-ignored by design. \`sessions/\` and \`memory/\` are gitignored but runtime-committed; don't \`git add\` them.
89
93
  - ${PACKAGE_JSON_INSTALL_RULE}
90
- - Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks.
94
+ - Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history in this folder unless the user explicitly asks. (Pushing a project clone you made elsewhere to open a PR is fine when the user asked for the PR.)
91
95
 
92
96
  ## How to behave
93
97
 
@@ -259,4 +263,6 @@ ${PACKAGE_JSON_INSTALL_RULE}
259
263
 
260
264
  Your free-write zone is \`workspace/\`. Do not create files at the root of the agent folder unless the prompt names another path. \`public/\` is the guest-visible zone — write there anything meant to be shared with an untrusted caller (a \`guest\`-role turn cannot read \`workspace/\` but can read \`public/\`). Do not edit \`memory/topics/\` directly — the dreaming subagent owns it; to capture something memorable, surface it in your reply or let the memory-logger append to \`memory/streams/\`. Never stage or commit \`secrets.json\`, \`.env\`, \`sessions/\`, \`memory/\`, or \`workspace/\` — those are runtime- or user-managed.
261
265
 
266
+ The agent folder is a private backup repo with no remote, not a project checkout. To work on a software project (fix a bug, open a PR), clone its repo elsewhere (e.g. \`/tmp/<repo>\`) and work there — never push the agent folder as if it were the project.
267
+
262
268
  See the session-origin block below for what kind of session this is and what's expected of you.`
@@ -39,10 +39,13 @@ export type ContinuationEpisode = {
39
39
  // The outcome of the most recently completed turn, recorded from the
40
40
  // `message_end` subscription (authoritative) or a prompt `finally` fallback.
41
41
  // `stopReason: 'unknown'` is the fail-closed value: an idle that sees it does
42
- // not auto-inject.
42
+ // not auto-inject. `'length'` is a budget truncation (the turn ran out of
43
+ // output tokens, often mid-thinking) — a legitimate unfinished turn that the
44
+ // continuation budget/stagnation guards are designed to bound, so it is
45
+ // continuation-eligible, NOT fail-closed.
43
46
  export type TurnOutcome = {
44
47
  turnId: string
45
- stopReason: 'stop' | 'aborted' | 'error' | 'unknown'
48
+ stopReason: 'stop' | 'length' | 'aborted' | 'error' | 'unknown'
46
49
  endedAt: number
47
50
  // Total tokens the just-completed turn consumed (from the assistant
48
51
  // message's usage). Accumulated into the episode's cumulativeTokens so the
@@ -73,7 +76,7 @@ export function emptyContinuationState(): ContinuationState {
73
76
  }
74
77
  }
75
78
 
76
- const STOP_REASONS = new Set<TurnOutcome['stopReason']>(['stop', 'aborted', 'error', 'unknown'])
79
+ const STOP_REASONS = new Set<TurnOutcome['stopReason']>(['stop', 'length', 'aborted', 'error', 'unknown'])
77
80
 
78
81
  // Validate a persisted state object field-by-field and fail closed: any field
79
82
  // that does not match the expected shape is dropped to its empty value rather
@@ -14,9 +14,11 @@ import { writeTodos } from './store'
14
14
 
15
15
  // Map a pi `message_end` event's stopReason onto the TurnOutcome stopReason
16
16
  // space. Anything we don't recognize collapses to 'unknown' so the idle path
17
- // fails closed (no auto-injection on an outcome we can't classify).
17
+ // fails closed (no auto-injection on an outcome we can't classify). 'length'
18
+ // is preserved (not collapsed) because a budget-truncated turn is a legitimate
19
+ // unfinished turn the continuation guards should be allowed to resume.
18
20
  export function classifyStopReason(raw: unknown): TurnOutcome['stopReason'] {
19
- if (raw === 'stop' || raw === 'aborted' || raw === 'error') return raw
21
+ if (raw === 'stop' || raw === 'length' || raw === 'aborted' || raw === 'error') return raw
20
22
  return 'unknown'
21
23
  }
22
24
 
@@ -16,9 +16,9 @@ export const CONTINUATION_PROMPT = [
16
16
  'cancelled) as you finish it by calling `todo_write` with the updated list. If',
17
17
  'you believe all the work is already done, do not just assert it — re-examine',
18
18
  'each remaining item skeptically, verify the work actually landed, and update',
19
- 'the list accordingly. When everything is genuinely complete, call',
20
- '`todo_clear`. Do not acknowledge or reply to this notice; just continue the',
21
- 'work.',
19
+ 'the list accordingly. Once your `todo_write` leaves no incomplete items, the',
20
+ 'list is cleared for you automatically. Do not acknowledge or reply to this',
21
+ 'notice; just continue the work.',
22
22
  '',
23
23
  '---',
24
24
  '',
@@ -50,7 +50,8 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
50
50
  '(restart, crash, or a later turn), you can resume the remaining work instead of silently ' +
51
51
  'dropping it. Mark items `completed` (or `cancelled`) as you finish them by writing the full ' +
52
52
  'list again with updated statuses. This is a full replace, not a merge: include every item ' +
53
- 'you still care about on each call.',
53
+ 'you still care about on each call. When the list you write has no incomplete items left, ' +
54
+ 'the runtime clears it for you — no separate cleanup call is needed.',
54
55
  parameters: Type.Object({
55
56
  todos: Type.Array(TODO_ITEM, { description: 'The complete todo list. Replaces any prior list.' }),
56
57
  }),
@@ -61,8 +62,28 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
61
62
  return { content: [{ type: 'text' as const, text: NO_SCOPE_NOTICE }], details }
62
63
  }
63
64
  const todos = params.todos as Todo[]
64
- await writeTodos(agentDir, scope, todos)
65
65
  const remaining = incompleteTodos(todos).length
66
+
67
+ // Collapse a fully-resolved list to empty in the SAME write that
68
+ // completed it, rather than relying on a follow-up todo_clear. That
69
+ // follow-up can be lost to an abort landing on the next turn, leaving a
70
+ // resolved list on disk (harmless to continuation, but it never gets
71
+ // cleaned up). Clearing here makes the cleanup race-free by construction.
72
+ if (remaining === 0 && todos.length > 0) {
73
+ await writeTodos(agentDir, scope, [])
74
+ const details: TodoToolDetails = { ok: true, total: todos.length, remaining: 0 }
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text' as const,
79
+ text: `All ${todos.length} todo(s) done; list cleared.`,
80
+ },
81
+ ],
82
+ details,
83
+ }
84
+ }
85
+
86
+ await writeTodos(agentDir, scope, todos)
66
87
  const details: TodoToolDetails = { ok: true, total: todos.length, remaining }
67
88
  return {
68
89
  content: [
@@ -100,8 +121,10 @@ export function createTodoTools({ agentDir, getOrigin }: CreateTodoToolsOptions)
100
121
  name: 'todo_clear',
101
122
  label: 'Clear Todos',
102
123
  description:
103
- 'Empty your todo list for this session. Call this when all work is genuinely done or the ' +
104
- 'task was abandoned, so the runtime stops tracking pending work.',
124
+ 'Empty your todo list for this session. Use this only to abandon a task with items still ' +
125
+ 'incomplete, so the runtime stops tracking pending work. A list with no incomplete items ' +
126
+ 'left is cleared automatically by `todo_write`, so you do not need to call this after ' +
127
+ 'finishing everything.',
105
128
  parameters: Type.Object({}),
106
129
  async execute() {
107
130
  const scope = scopeForOrigin(getOrigin)