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.
- package/README.md +6 -6
- package/package.json +5 -3
- package/scripts/require-parallel.ts +41 -0
- package/src/agent/index.ts +55 -6
- package/src/agent/live-sessions.ts +34 -0
- package/src/agent/plugin-tools.ts +2 -0
- package/src/agent/session-meta.ts +21 -2
- package/src/agent/subagent-completion-reminder.ts +89 -0
- package/src/agent/subagents.ts +75 -15
- package/src/agent/system-prompt.ts +10 -8
- package/src/agent/tools/channel-reply.ts +47 -7
- package/src/agent/tools/channel-send.ts +43 -11
- package/src/agent/tools/runtime-notice.ts +41 -0
- package/src/bundled-plugins/explorer/explorer.ts +2 -2
- package/src/bundled-plugins/guard/index.ts +14 -1
- package/src/bundled-plugins/guard/policies/managed-config.ts +43 -13
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +37 -0
- package/src/bundled-plugins/guard/policies/memory-topics-delete.ts +67 -0
- package/src/bundled-plugins/guard/policies/memory-topics-write.ts +33 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +8 -2
- package/src/bundled-plugins/guard/policy.ts +7 -0
- package/src/bundled-plugins/memory/README.md +76 -62
- package/src/bundled-plugins/memory/append-tool.ts +3 -2
- package/src/bundled-plugins/memory/citation-superset.ts +49 -11
- package/src/bundled-plugins/memory/citations.ts +19 -8
- package/src/bundled-plugins/memory/delete-tool.ts +57 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +1 -1
- package/src/bundled-plugins/memory/dreaming.ts +364 -146
- package/src/bundled-plugins/memory/frontmatter.ts +165 -0
- package/src/bundled-plugins/memory/index.ts +257 -16
- package/src/bundled-plugins/memory/injection-plan.ts +15 -0
- package/src/bundled-plugins/memory/load-memory.ts +102 -103
- package/src/bundled-plugins/memory/load-shards.ts +156 -0
- package/src/bundled-plugins/memory/memory-logger.ts +16 -15
- package/src/bundled-plugins/memory/memory-retrieval.ts +111 -0
- package/src/bundled-plugins/memory/migration.ts +353 -1
- package/src/bundled-plugins/memory/paths.ts +42 -0
- package/src/bundled-plugins/memory/search-tool.ts +232 -0
- package/src/bundled-plugins/memory/secret-detector.ts +2 -2
- package/src/bundled-plugins/memory/shard-snapshot.ts +51 -0
- package/src/bundled-plugins/memory/slug.ts +59 -0
- package/src/bundled-plugins/memory/stream-io.ts +110 -1
- package/src/bundled-plugins/memory/strength.ts +3 -3
- package/src/bundled-plugins/memory/topics.ts +70 -16
- package/src/bundled-plugins/security/index.ts +24 -0
- package/src/bundled-plugins/security/permissions.ts +4 -0
- package/src/bundled-plugins/security/policies/cron-promotion.ts +349 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +2 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +3 -0
- package/src/bundled-plugins/security/policies/role-promotion.ts +419 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +1 -0
- package/src/channels/adapters/kakaotalk-attachment.ts +7 -17
- package/src/channels/adapters/kakaotalk-classify.ts +4 -1
- package/src/channels/adapters/kakaotalk.ts +65 -38
- package/src/channels/adapters/slack-bot-classify.ts +2 -27
- package/src/channels/index.ts +5 -0
- package/src/channels/router.ts +320 -22
- package/src/channels/subagent-completion-bridge.ts +84 -0
- package/src/cli/builtins.ts +1 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/init.ts +122 -14
- package/src/cli/inspect.ts +151 -0
- package/src/cron/consumer.ts +1 -1
- package/src/init/dockerfile.ts +268 -4
- package/src/init/hatching.ts +5 -6
- package/src/init/kakaotalk-auth.ts +6 -47
- package/src/init/validate-api-key.ts +121 -0
- package/src/inspect/index.ts +213 -0
- package/src/inspect/label.ts +50 -0
- package/src/inspect/live.ts +221 -0
- package/src/inspect/render.ts +163 -0
- package/src/inspect/replay.ts +295 -0
- package/src/inspect/session-list.ts +160 -0
- package/src/inspect/types.ts +110 -0
- package/src/plugin/hooks.ts +23 -1
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +1 -1
- package/src/plugin/registry.ts +1 -1
- package/src/plugin/types.ts +10 -0
- package/src/run/channel-session-factory.ts +7 -1
- package/src/run/index.ts +103 -21
- package/src/secrets/kakao-renewal.ts +3 -47
- package/src/server/index.ts +241 -60
- package/src/shared/index.ts +3 -0
- package/src/shared/protocol.ts +49 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +9 -9
- package/src/skills/typeclaw-claude-code/SKILL.md +57 -39
- package/src/skills/typeclaw-claude-code/references/stop-hook.md +2 -0
- package/src/skills/typeclaw-claude-code/references/tmux-driving.md +102 -16
- package/src/skills/typeclaw-config/SKILL.md +1 -1
- package/src/skills/typeclaw-cron/SKILL.md +1 -1
- package/src/skills/typeclaw-memory/SKILL.md +16 -163
- package/src/skills/typeclaw-permissions/SKILL.md +2 -2
- package/src/skills/typeclaw-plugins/SKILL.md +25 -14
- package/src/test-helpers/wait-for.ts +7 -1
- 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 {
|
|
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: () =>
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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 ??
|
|
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
|
-
}
|