typeclaw 0.8.0 → 0.9.1

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 (96) 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 +75 -15
  10. package/src/agent/system-prompt.ts +10 -8
  11. package/src/agent/tools/channel-reply.ts +47 -7
  12. package/src/agent/tools/channel-send.ts +43 -11
  13. package/src/agent/tools/runtime-notice.ts +41 -0
  14. package/src/bundled-plugins/explorer/explorer.ts +2 -2
  15. package/src/bundled-plugins/guard/index.ts +14 -1
  16. package/src/bundled-plugins/guard/policies/managed-config.ts +43 -13
  17. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +37 -0
  18. package/src/bundled-plugins/guard/policies/memory-topics-delete.ts +67 -0
  19. package/src/bundled-plugins/guard/policies/memory-topics-write.ts +33 -0
  20. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +8 -2
  21. package/src/bundled-plugins/guard/policy.ts +7 -0
  22. package/src/bundled-plugins/memory/README.md +76 -62
  23. package/src/bundled-plugins/memory/append-tool.ts +3 -2
  24. package/src/bundled-plugins/memory/citation-superset.ts +49 -11
  25. package/src/bundled-plugins/memory/citations.ts +19 -8
  26. package/src/bundled-plugins/memory/delete-tool.ts +57 -0
  27. package/src/bundled-plugins/memory/dreaming-state.ts +1 -1
  28. package/src/bundled-plugins/memory/dreaming.ts +364 -146
  29. package/src/bundled-plugins/memory/frontmatter.ts +165 -0
  30. package/src/bundled-plugins/memory/index.ts +257 -16
  31. package/src/bundled-plugins/memory/injection-plan.ts +15 -0
  32. package/src/bundled-plugins/memory/load-memory.ts +102 -103
  33. package/src/bundled-plugins/memory/load-shards.ts +156 -0
  34. package/src/bundled-plugins/memory/memory-logger.ts +16 -15
  35. package/src/bundled-plugins/memory/memory-retrieval.ts +111 -0
  36. package/src/bundled-plugins/memory/migration.ts +353 -1
  37. package/src/bundled-plugins/memory/paths.ts +42 -0
  38. package/src/bundled-plugins/memory/search-tool.ts +232 -0
  39. package/src/bundled-plugins/memory/secret-detector.ts +2 -2
  40. package/src/bundled-plugins/memory/shard-snapshot.ts +51 -0
  41. package/src/bundled-plugins/memory/slug.ts +59 -0
  42. package/src/bundled-plugins/memory/stream-io.ts +110 -1
  43. package/src/bundled-plugins/memory/strength.ts +3 -3
  44. package/src/bundled-plugins/memory/topics.ts +70 -16
  45. package/src/bundled-plugins/security/index.ts +24 -0
  46. package/src/bundled-plugins/security/permissions.ts +4 -0
  47. package/src/bundled-plugins/security/policies/cron-promotion.ts +349 -0
  48. package/src/bundled-plugins/security/policies/git-exfil.ts +2 -0
  49. package/src/bundled-plugins/security/policies/prompt-injection.ts +3 -0
  50. package/src/bundled-plugins/security/policies/role-promotion.ts +419 -0
  51. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +1 -0
  52. package/src/channels/adapters/kakaotalk-attachment.ts +7 -17
  53. package/src/channels/adapters/kakaotalk-classify.ts +4 -1
  54. package/src/channels/adapters/kakaotalk.ts +65 -38
  55. package/src/channels/adapters/slack-bot-classify.ts +2 -27
  56. package/src/channels/index.ts +5 -0
  57. package/src/channels/router.ts +320 -22
  58. package/src/channels/subagent-completion-bridge.ts +84 -0
  59. package/src/cli/builtins.ts +1 -0
  60. package/src/cli/index.ts +1 -0
  61. package/src/cli/init.ts +122 -14
  62. package/src/cli/inspect.ts +151 -0
  63. package/src/cron/consumer.ts +1 -1
  64. package/src/init/dockerfile.ts +268 -4
  65. package/src/init/hatching.ts +5 -6
  66. package/src/init/kakaotalk-auth.ts +6 -47
  67. package/src/init/validate-api-key.ts +121 -0
  68. package/src/inspect/index.ts +213 -0
  69. package/src/inspect/label.ts +50 -0
  70. package/src/inspect/live.ts +221 -0
  71. package/src/inspect/render.ts +163 -0
  72. package/src/inspect/replay.ts +295 -0
  73. package/src/inspect/session-list.ts +160 -0
  74. package/src/inspect/types.ts +110 -0
  75. package/src/plugin/hooks.ts +23 -1
  76. package/src/plugin/index.ts +2 -0
  77. package/src/plugin/manager.ts +1 -1
  78. package/src/plugin/registry.ts +1 -1
  79. package/src/plugin/types.ts +10 -0
  80. package/src/run/channel-session-factory.ts +7 -1
  81. package/src/run/index.ts +103 -21
  82. package/src/secrets/kakao-renewal.ts +3 -47
  83. package/src/server/index.ts +241 -60
  84. package/src/shared/index.ts +3 -0
  85. package/src/shared/protocol.ts +49 -0
  86. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +9 -9
  87. package/src/skills/typeclaw-claude-code/SKILL.md +57 -39
  88. package/src/skills/typeclaw-claude-code/references/stop-hook.md +2 -0
  89. package/src/skills/typeclaw-claude-code/references/tmux-driving.md +102 -16
  90. package/src/skills/typeclaw-config/SKILL.md +1 -1
  91. package/src/skills/typeclaw-cron/SKILL.md +1 -1
  92. package/src/skills/typeclaw-memory/SKILL.md +16 -163
  93. package/src/skills/typeclaw-permissions/SKILL.md +2 -2
  94. package/src/skills/typeclaw-plugins/SKILL.md +25 -14
  95. package/src/test-helpers/wait-for.ts +7 -1
  96. package/typeclaw.schema.json +15 -1
package/src/run/index.ts CHANGED
@@ -1,19 +1,28 @@
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 {
8
+ awaitWithSubagentTimeout,
7
9
  createSubagentConsumer,
8
10
  defaultCreateSessionForSubagent,
9
11
  invokeSubagent,
12
+ isSubagentTimeoutError,
10
13
  type Subagent as InternalSubagent,
11
14
  type SubagentConsumer,
12
15
  type SubagentRegistry,
13
16
  type SubagentShared,
14
17
  } from '@/agent/subagents'
15
18
  import { resolveCapOptionsFromConfig } from '@/bundled-plugins/tool-result-cap'
16
- import { createChannelManager, createChannelsReloadable, type ChannelManager } from '@/channels'
19
+ import {
20
+ createChannelManager,
21
+ createChannelsReloadable,
22
+ createSubagentCompletionBridge,
23
+ type ChannelManager,
24
+ type SubagentCompletionBridge,
25
+ } from '@/channels'
17
26
  import { createTunnelBridge, type TunnelBridge } from '@/channels/tunnel-bridge'
18
27
  import { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync } from '@/config'
19
28
  import {
@@ -179,6 +188,7 @@ export async function startAgent({
179
188
  })
180
189
 
181
190
  const liveSubagentRegistry = new LiveSubagentRegistry()
191
+ const liveSessionRegistry = new LiveSessionRegistry()
182
192
 
183
193
  const channelManager = createChannelManagerFor({
184
194
  agentDir: cwd,
@@ -196,6 +206,7 @@ export async function startAgent({
196
206
  rehydrateCapOptions: resolveCapOptionsFromConfig(pluginConfigsByName['tool-result-cap']),
197
207
  permissions: pluginsLoaded.permissions,
198
208
  liveSubagentRegistry,
209
+ liveSessionRegistry,
199
210
  subagentRegistry: pluginRuntime.get().subagents,
200
211
  getCreateSessionForSubagent: () => createSessionForSubagent,
201
212
  ...containerNameOpt,
@@ -245,8 +256,14 @@ export async function startAgent({
245
256
  : {}),
246
257
  ...runtimeVersionOpt,
247
258
  })
259
+ liveSessionRegistry.register({ sessionId, session: created.session })
260
+ const originalDispose = created.dispose
248
261
  return {
249
262
  ...created,
263
+ dispose: async () => {
264
+ liveSessionRegistry.unregister(sessionId)
265
+ await originalDispose()
266
+ },
250
267
  hooks: snap.hooks,
251
268
  sessionId,
252
269
  agentDir: cwd,
@@ -360,9 +377,13 @@ export async function startAgent({
360
377
  ...containerNameOpt,
361
378
  ...runtimeVersionOpt,
362
379
  })
380
+ liveSessionRegistry.register({ sessionId, session })
363
381
  return {
364
382
  prompt: (text) => session.prompt(text),
365
- dispose: () => session.dispose(),
383
+ dispose: () => {
384
+ liveSessionRegistry.unregister(sessionId)
385
+ session.dispose()
386
+ },
366
387
  sessionId,
367
388
  agentDir: cwd,
368
389
  origin: cronOrigin,
@@ -393,32 +414,91 @@ export async function startAgent({
393
414
 
394
415
  const tunnelBridge: TunnelBridge = createTunnelBridge({ stream, channelManager })
395
416
 
417
+ // Bridge `subagent.completed` broadcasts into the channel router so a
418
+ // backgrounded subagent finishing wakes up its parent channel session
419
+ // with a `<system-reminder>` — symmetric to the TUI bridge in
420
+ // src/server/index.ts. Must be created BEFORE channelManager.start()
421
+ // so an initial broadcast can never race past the subscription gap.
422
+ const subagentCompletionBridge: SubagentCompletionBridge = createSubagentCompletionBridge({
423
+ stream,
424
+ router: channelManager.router,
425
+ })
426
+
396
427
  reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
397
428
  await channelManager.start()
398
429
 
399
430
  // Captured separately from setSpawnSubagent so both the plugin context and
400
431
  // the plugin-command runner can dispatch through the same path. The setter
401
432
  // returns void, so without this local binding we couldn't reuse the fn.
433
+ //
434
+ // In-flight coalescing for direct ctx.spawnSubagent calls mirrors the
435
+ // SubagentConsumer's stream-path gate (subagents.ts:441). Two queued
436
+ // `new-session` messages for the same (name, inFlightKey) drop the second
437
+ // on the consumer side; without the same gate here, two consecutive
438
+ // session.prompt fires (cold-start prompt N immediately followed by prompt
439
+ // N+1 on the same channel session) could both fire memory-retrieval spawns
440
+ // racing to write `memory/.retrieval-cache/<sessionId>.md`. Awaiting the
441
+ // spawn in the hook used to mask this; now that the hook is fire-and-forget,
442
+ // the race is exposed and the gate is mandatory.
443
+ //
444
+ // Same key shape as the consumer: `${name}:${inFlightKey(payload)}` when the
445
+ // subagent declares one, else just `${name}`. Collisions resolve cleanly
446
+ // (logged + return) instead of rejecting, because callers from
447
+ // session.prompt are detached and a colliding spawn is a noop, not an error.
448
+ const directSpawnInFlight = new Set<string>()
402
449
  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
- })
450
+ const entry = pluginSubagentByName.get(name)
451
+ const keyFn = entry?.pluginSubagent.inFlightKey
452
+ let coalesceKey = name
453
+ if (keyFn !== undefined) {
454
+ try {
455
+ coalesceKey = `${name}:${keyFn(payload)}`
456
+ } catch {
457
+ coalesceKey = name
458
+ }
459
+ }
460
+ if (directSpawnInFlight.has(coalesceKey)) {
461
+ console.warn(`[subagent] ${coalesceKey}: previous direct spawn still in progress, skipping`)
462
+ return
463
+ }
464
+ directSpawnInFlight.add(coalesceKey)
465
+ try {
466
+ // Resolve the spawning session's role from its origin so the subagent
467
+ // inherits it. Callers (hooks like session.idle) pass the parent origin
468
+ // verbatim; we look up the role rather than letting the caller forge it,
469
+ // closing the laundering vector the design doc calls out for cron.
470
+ const spawnedByRole =
471
+ options?.spawnedByOrigin !== undefined
472
+ ? pluginsLoaded.permissions.resolveRole(options.spawnedByOrigin)
473
+ : undefined
474
+ const registry = pluginRuntime.get().subagents
475
+ try {
476
+ await awaitWithSubagentTimeout(
477
+ invokeSubagent(name, {
478
+ registry,
479
+ createSessionForSubagent,
480
+ agentDir: cwd,
481
+ userPrompt: '',
482
+ payload,
483
+ onProviderError: (message) => console.error(`[subagent] ${name}: LLM call failed: ${message}`),
484
+ ...(options?.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
485
+ ...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
486
+ ...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
487
+ }),
488
+ name,
489
+ coalesceKey,
490
+ registry[name]?.timeoutMs,
491
+ )
492
+ } catch (err) {
493
+ if (isSubagentTimeoutError(err)) {
494
+ console.warn(`[subagent] ${coalesceKey} timed out after ${err.timeoutMs}ms; releasing coalesce key`)
495
+ return
496
+ }
497
+ throw err
498
+ }
499
+ } finally {
500
+ directSpawnInFlight.delete(coalesceKey)
501
+ }
422
502
  }
423
503
  pluginsLoaded.setSpawnSubagent(dispatchSpawnSubagent)
424
504
  pluginsLoaded.markBooted()
@@ -477,6 +557,7 @@ export async function startAgent({
477
557
  tunnelManager,
478
558
  liveSubagentRegistry,
479
559
  createSessionForSubagent,
560
+ liveSessionRegistry,
480
561
  ...containerNameOpt,
481
562
  ...runtimeVersionOpt,
482
563
  ...tuiTokenOpt,
@@ -500,6 +581,7 @@ export async function startAgent({
500
581
  server.stop(true)
501
582
  void disposeMaterializedSkills(pluginRuntime)
502
583
  tunnelBridge.stop()
584
+ subagentCompletionBridge.stop()
503
585
  await tunnelManager.stop()
504
586
  await channelManager.stop()
505
587
  }
@@ -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
- }