typeclaw 0.8.0 → 0.9.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 (92) hide show
  1. package/README.md +6 -6
  2. package/package.json +5 -3
  3. package/scripts/require-parallel.ts +41 -0
  4. package/src/agent/index.ts +55 -6
  5. package/src/agent/live-sessions.ts +34 -0
  6. package/src/agent/plugin-tools.ts +2 -0
  7. package/src/agent/session-meta.ts +21 -2
  8. package/src/agent/subagent-completion-reminder.ts +89 -0
  9. package/src/agent/subagents.ts +3 -2
  10. package/src/agent/system-prompt.ts +10 -8
  11. package/src/bundled-plugins/explorer/explorer.ts +2 -2
  12. package/src/bundled-plugins/guard/index.ts +14 -1
  13. package/src/bundled-plugins/guard/policies/managed-config.ts +43 -13
  14. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +37 -0
  15. package/src/bundled-plugins/guard/policies/memory-topics-delete.ts +67 -0
  16. package/src/bundled-plugins/guard/policies/memory-topics-write.ts +33 -0
  17. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +8 -2
  18. package/src/bundled-plugins/guard/policy.ts +7 -0
  19. package/src/bundled-plugins/memory/README.md +76 -62
  20. package/src/bundled-plugins/memory/append-tool.ts +3 -2
  21. package/src/bundled-plugins/memory/citation-superset.ts +49 -11
  22. package/src/bundled-plugins/memory/citations.ts +19 -8
  23. package/src/bundled-plugins/memory/delete-tool.ts +57 -0
  24. package/src/bundled-plugins/memory/dreaming-state.ts +1 -1
  25. package/src/bundled-plugins/memory/dreaming.ts +364 -146
  26. package/src/bundled-plugins/memory/frontmatter.ts +165 -0
  27. package/src/bundled-plugins/memory/index.ts +236 -16
  28. package/src/bundled-plugins/memory/injection-plan.ts +15 -0
  29. package/src/bundled-plugins/memory/load-memory.ts +102 -103
  30. package/src/bundled-plugins/memory/load-shards.ts +156 -0
  31. package/src/bundled-plugins/memory/memory-logger.ts +16 -15
  32. package/src/bundled-plugins/memory/memory-retrieval.ts +105 -0
  33. package/src/bundled-plugins/memory/migration.ts +282 -1
  34. package/src/bundled-plugins/memory/paths.ts +42 -0
  35. package/src/bundled-plugins/memory/search-tool.ts +232 -0
  36. package/src/bundled-plugins/memory/secret-detector.ts +2 -2
  37. package/src/bundled-plugins/memory/shard-snapshot.ts +51 -0
  38. package/src/bundled-plugins/memory/slug.ts +59 -0
  39. package/src/bundled-plugins/memory/stream-io.ts +110 -1
  40. package/src/bundled-plugins/memory/strength.ts +3 -3
  41. package/src/bundled-plugins/memory/topics.ts +70 -16
  42. package/src/bundled-plugins/security/index.ts +24 -0
  43. package/src/bundled-plugins/security/permissions.ts +4 -0
  44. package/src/bundled-plugins/security/policies/cron-promotion.ts +349 -0
  45. package/src/bundled-plugins/security/policies/git-exfil.ts +2 -0
  46. package/src/bundled-plugins/security/policies/prompt-injection.ts +3 -0
  47. package/src/bundled-plugins/security/policies/role-promotion.ts +419 -0
  48. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +1 -0
  49. package/src/channels/adapters/kakaotalk-attachment.ts +7 -17
  50. package/src/channels/adapters/kakaotalk.ts +64 -37
  51. package/src/channels/adapters/slack-bot-classify.ts +2 -27
  52. package/src/channels/index.ts +5 -0
  53. package/src/channels/router.ts +201 -17
  54. package/src/channels/subagent-completion-bridge.ts +84 -0
  55. package/src/cli/builtins.ts +1 -0
  56. package/src/cli/index.ts +1 -0
  57. package/src/cli/init.ts +122 -14
  58. package/src/cli/inspect.ts +151 -0
  59. package/src/cron/consumer.ts +1 -1
  60. package/src/init/dockerfile.ts +268 -4
  61. package/src/init/hatching.ts +5 -6
  62. package/src/init/kakaotalk-auth.ts +6 -47
  63. package/src/init/validate-api-key.ts +121 -0
  64. package/src/inspect/index.ts +213 -0
  65. package/src/inspect/label.ts +50 -0
  66. package/src/inspect/live.ts +221 -0
  67. package/src/inspect/render.ts +163 -0
  68. package/src/inspect/replay.ts +265 -0
  69. package/src/inspect/session-list.ts +160 -0
  70. package/src/inspect/types.ts +110 -0
  71. package/src/plugin/hooks.ts +23 -1
  72. package/src/plugin/index.ts +2 -0
  73. package/src/plugin/manager.ts +1 -1
  74. package/src/plugin/registry.ts +1 -1
  75. package/src/plugin/types.ts +10 -0
  76. package/src/run/channel-session-factory.ts +7 -1
  77. package/src/run/index.ts +87 -21
  78. package/src/secrets/kakao-renewal.ts +3 -47
  79. package/src/server/index.ts +241 -60
  80. package/src/shared/index.ts +3 -0
  81. package/src/shared/protocol.ts +49 -0
  82. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +9 -9
  83. package/src/skills/typeclaw-claude-code/SKILL.md +57 -39
  84. package/src/skills/typeclaw-claude-code/references/stop-hook.md +2 -0
  85. package/src/skills/typeclaw-claude-code/references/tmux-driving.md +102 -16
  86. package/src/skills/typeclaw-config/SKILL.md +1 -1
  87. package/src/skills/typeclaw-cron/SKILL.md +1 -1
  88. package/src/skills/typeclaw-memory/SKILL.md +16 -163
  89. package/src/skills/typeclaw-permissions/SKILL.md +2 -2
  90. package/src/skills/typeclaw-plugins/SKILL.md +25 -14
  91. package/src/test-helpers/wait-for.ts +7 -1
  92. package/typeclaw.schema.json +7 -0
package/src/run/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SessionManager } from '@mariozechner/pi-coding-agent'
2
2
 
3
3
  import { createSession, createSessionWithDispose } from '@/agent'
4
+ import { LiveSessionRegistry } from '@/agent/live-sessions'
4
5
  import { LiveSubagentRegistry } from '@/agent/live-subagents'
5
6
  import type { SessionOrigin } from '@/agent/session-origin'
6
7
  import {
@@ -13,7 +14,13 @@ import {
13
14
  type SubagentShared,
14
15
  } from '@/agent/subagents'
15
16
  import { resolveCapOptionsFromConfig } from '@/bundled-plugins/tool-result-cap'
16
- import { createChannelManager, createChannelsReloadable, type ChannelManager } from '@/channels'
17
+ import {
18
+ createChannelManager,
19
+ createChannelsReloadable,
20
+ createSubagentCompletionBridge,
21
+ type ChannelManager,
22
+ type SubagentCompletionBridge,
23
+ } from '@/channels'
17
24
  import { createTunnelBridge, type TunnelBridge } from '@/channels/tunnel-bridge'
18
25
  import { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync } from '@/config'
19
26
  import {
@@ -179,6 +186,7 @@ export async function startAgent({
179
186
  })
180
187
 
181
188
  const liveSubagentRegistry = new LiveSubagentRegistry()
189
+ const liveSessionRegistry = new LiveSessionRegistry()
182
190
 
183
191
  const channelManager = createChannelManagerFor({
184
192
  agentDir: cwd,
@@ -196,6 +204,7 @@ export async function startAgent({
196
204
  rehydrateCapOptions: resolveCapOptionsFromConfig(pluginConfigsByName['tool-result-cap']),
197
205
  permissions: pluginsLoaded.permissions,
198
206
  liveSubagentRegistry,
207
+ liveSessionRegistry,
199
208
  subagentRegistry: pluginRuntime.get().subagents,
200
209
  getCreateSessionForSubagent: () => createSessionForSubagent,
201
210
  ...containerNameOpt,
@@ -245,8 +254,14 @@ export async function startAgent({
245
254
  : {}),
246
255
  ...runtimeVersionOpt,
247
256
  })
257
+ liveSessionRegistry.register({ sessionId, session: created.session })
258
+ const originalDispose = created.dispose
248
259
  return {
249
260
  ...created,
261
+ dispose: async () => {
262
+ liveSessionRegistry.unregister(sessionId)
263
+ await originalDispose()
264
+ },
250
265
  hooks: snap.hooks,
251
266
  sessionId,
252
267
  agentDir: cwd,
@@ -360,9 +375,13 @@ export async function startAgent({
360
375
  ...containerNameOpt,
361
376
  ...runtimeVersionOpt,
362
377
  })
378
+ liveSessionRegistry.register({ sessionId, session })
363
379
  return {
364
380
  prompt: (text) => session.prompt(text),
365
- dispose: () => session.dispose(),
381
+ dispose: () => {
382
+ liveSessionRegistry.unregister(sessionId)
383
+ session.dispose()
384
+ },
366
385
  sessionId,
367
386
  agentDir: cwd,
368
387
  origin: cronOrigin,
@@ -393,32 +412,77 @@ export async function startAgent({
393
412
 
394
413
  const tunnelBridge: TunnelBridge = createTunnelBridge({ stream, channelManager })
395
414
 
415
+ // Bridge `subagent.completed` broadcasts into the channel router so a
416
+ // backgrounded subagent finishing wakes up its parent channel session
417
+ // with a `<system-reminder>` — symmetric to the TUI bridge in
418
+ // src/server/index.ts. Must be created BEFORE channelManager.start()
419
+ // so an initial broadcast can never race past the subscription gap.
420
+ const subagentCompletionBridge: SubagentCompletionBridge = createSubagentCompletionBridge({
421
+ stream,
422
+ router: channelManager.router,
423
+ })
424
+
396
425
  reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
397
426
  await channelManager.start()
398
427
 
399
428
  // Captured separately from setSpawnSubagent so both the plugin context and
400
429
  // the plugin-command runner can dispatch through the same path. The setter
401
430
  // returns void, so without this local binding we couldn't reuse the fn.
431
+ //
432
+ // In-flight coalescing for direct ctx.spawnSubagent calls mirrors the
433
+ // SubagentConsumer's stream-path gate (subagents.ts:441). Two queued
434
+ // `new-session` messages for the same (name, inFlightKey) drop the second
435
+ // on the consumer side; without the same gate here, two consecutive
436
+ // session.prompt fires (cold-start prompt N immediately followed by prompt
437
+ // N+1 on the same channel session) could both fire memory-retrieval spawns
438
+ // racing to write `memory/.retrieval-cache/<sessionId>.md`. Awaiting the
439
+ // spawn in the hook used to mask this; now that the hook is fire-and-forget,
440
+ // the race is exposed and the gate is mandatory.
441
+ //
442
+ // Same key shape as the consumer: `${name}:${inFlightKey(payload)}` when the
443
+ // subagent declares one, else just `${name}`. Collisions resolve cleanly
444
+ // (logged + return) instead of rejecting, because callers from
445
+ // session.prompt are detached and a colliding spawn is a noop, not an error.
446
+ const directSpawnInFlight = new Set<string>()
402
447
  const dispatchSpawnSubagent: CommandSpawnSubagent = async (name, payload, options) => {
403
- // Resolve the spawning session's role from its origin so the subagent
404
- // inherits it. Callers (hooks like session.idle) pass the parent origin
405
- // verbatim; we look up the role rather than letting the caller forge it,
406
- // closing the laundering vector the design doc calls out for cron.
407
- const spawnedByRole =
408
- options?.spawnedByOrigin !== undefined
409
- ? pluginsLoaded.permissions.resolveRole(options.spawnedByOrigin)
410
- : undefined
411
- await invokeSubagent(name, {
412
- registry: pluginRuntime.get().subagents,
413
- createSessionForSubagent,
414
- agentDir: cwd,
415
- userPrompt: '',
416
- payload,
417
- onProviderError: (message) => console.error(`[subagent] ${name}: LLM call failed: ${message}`),
418
- ...(options?.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
419
- ...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
420
- ...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
421
- })
448
+ const entry = pluginSubagentByName.get(name)
449
+ const keyFn = entry?.pluginSubagent.inFlightKey
450
+ let coalesceKey = name
451
+ if (keyFn !== undefined) {
452
+ try {
453
+ coalesceKey = `${name}:${keyFn(payload)}`
454
+ } catch {
455
+ coalesceKey = name
456
+ }
457
+ }
458
+ if (directSpawnInFlight.has(coalesceKey)) {
459
+ console.warn(`[subagent] ${coalesceKey}: previous direct spawn still in progress, skipping`)
460
+ return
461
+ }
462
+ directSpawnInFlight.add(coalesceKey)
463
+ try {
464
+ // Resolve the spawning session's role from its origin so the subagent
465
+ // inherits it. Callers (hooks like session.idle) pass the parent origin
466
+ // verbatim; we look up the role rather than letting the caller forge it,
467
+ // closing the laundering vector the design doc calls out for cron.
468
+ const spawnedByRole =
469
+ options?.spawnedByOrigin !== undefined
470
+ ? pluginsLoaded.permissions.resolveRole(options.spawnedByOrigin)
471
+ : undefined
472
+ await invokeSubagent(name, {
473
+ registry: pluginRuntime.get().subagents,
474
+ createSessionForSubagent,
475
+ agentDir: cwd,
476
+ userPrompt: '',
477
+ payload,
478
+ onProviderError: (message) => console.error(`[subagent] ${name}: LLM call failed: ${message}`),
479
+ ...(options?.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
480
+ ...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
481
+ ...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
482
+ })
483
+ } finally {
484
+ directSpawnInFlight.delete(coalesceKey)
485
+ }
422
486
  }
423
487
  pluginsLoaded.setSpawnSubagent(dispatchSpawnSubagent)
424
488
  pluginsLoaded.markBooted()
@@ -477,6 +541,7 @@ export async function startAgent({
477
541
  tunnelManager,
478
542
  liveSubagentRegistry,
479
543
  createSessionForSubagent,
544
+ liveSessionRegistry,
480
545
  ...containerNameOpt,
481
546
  ...runtimeVersionOpt,
482
547
  ...tuiTokenOpt,
@@ -500,6 +565,7 @@ export async function startAgent({
500
565
  server.stop(true)
501
566
  void disposeMaterializedSkills(pluginRuntime)
502
567
  tunnelBridge.stop()
568
+ subagentCompletionBridge.stop()
503
569
  await tunnelManager.stop()
504
570
  await channelManager.stop()
505
571
  }
@@ -1,7 +1,6 @@
1
- import { createRequire } from 'node:module'
2
1
  import { join } from 'node:path'
3
2
 
4
- import type { KakaoDeviceType } from 'agent-messenger/kakaotalk'
3
+ import { attemptLogin as upstreamAttemptLogin, type KakaoDeviceType } from 'agent-messenger/kakaotalk'
5
4
 
6
5
  import { decrypt, EncryptionError } from './encryption'
7
6
  import { SecretsKakaoCredentialStore } from './kakao-store'
@@ -9,21 +8,6 @@ import { type KeyStore, KeyStoreError } from './keys'
9
8
  import { type KakaoChannelBlock } from './schema'
10
9
  import { SecretsBackend } from './storage'
11
10
 
12
- // Mirrors KakaoLoginResult from agent-messenger/kakaotalk's types.d.ts. The
13
- // upstream interface is not re-exported from the package root, so we declare
14
- // the structural shape locally. If a future version adds new fields, this
15
- // stays forward-compatible because we only read the ones declared here.
16
- export type KakaoLoginResult = {
17
- authenticated: boolean
18
- next_action?: string
19
- message?: string
20
- warning?: string
21
- account_id?: string
22
- device_type?: KakaoDeviceType
23
- user_id?: string
24
- error?: string
25
- }
26
-
27
11
  export const RENEWAL_THRESHOLD_MS = 5 * 24 * 60 * 60 * 1000
28
12
 
29
13
  // Hard ~7-day TTL on KakaoTalk sub-device tokens means renewal must happen
@@ -50,21 +34,7 @@ export type RenewalAttempt =
50
34
  | { kind: 'reauth_required'; account_id: string; reason: string; message: string }
51
35
  | { kind: 'transient_failure'; account_id: string; reason: string }
52
36
 
53
- export type AttemptLoginFn = (
54
- email: string,
55
- password: string,
56
- deviceUuid: string,
57
- deviceType: KakaoDeviceType,
58
- forced: boolean,
59
- ) => Promise<KakaoLoginResult & { credentials?: LoginCredentials }>
60
-
61
- export type LoginCredentials = {
62
- access_token: string
63
- refresh_token: string
64
- user_id: string
65
- device_uuid: string
66
- device_type: KakaoDeviceType
67
- }
37
+ export type AttemptLoginFn = typeof upstreamAttemptLogin
68
38
 
69
39
  export type RenewalContext = {
70
40
  containerName: string
@@ -151,7 +121,7 @@ export async function renewCurrentAccount(
151
121
  }
152
122
  }
153
123
 
154
- const attemptLogin = ctx.attemptLogin ?? (await resolveAttemptLogin())
124
+ const attemptLogin = ctx.attemptLogin ?? upstreamAttemptLogin
155
125
  const result = await attemptLogin(
156
126
  decision.account.email,
157
127
  decision.password,
@@ -232,17 +202,3 @@ function classifyDecryptFailure(err: unknown, accountId: string): RenewalDecisio
232
202
  message: `Could not decrypt stored KakaoTalk password (${err instanceof Error ? err.message : String(err)}).`,
233
203
  }
234
204
  }
235
-
236
- async function resolveAttemptLogin(): Promise<AttemptLoginFn> {
237
- // agent-messenger does not export `attemptLogin` from its public exports
238
- // map. Resolve the package's installed location and import the auth
239
- // implementation file directly — same pattern as runKakaotalkBootstrap's
240
- // loginFlow resolution in src/init/kakaotalk-auth.ts.
241
- const require = createRequire(import.meta.url)
242
- const pkgJson = require.resolve('agent-messenger/package.json')
243
- const pkgDir = pkgJson.replace(/\/package\.json$/, '')
244
- const mod = (await import(`${pkgDir}/dist/src/platforms/kakaotalk/auth/kakao-login.js`)) as {
245
- attemptLogin: AttemptLoginFn
246
- }
247
- return mod.attemptLogin
248
- }