@dsh-cc/subagent-task 0.5.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.
- package/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +214 -0
- package/README.zh.md +100 -0
- package/lib/background-start.d.ts +207 -0
- package/lib/background-start.d.ts.map +1 -0
- package/lib/background-start.js +354 -0
- package/lib/background-start.js.map +1 -0
- package/lib/catalog.d.ts +99 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +197 -0
- package/lib/catalog.js.map +1 -0
- package/lib/epoch-collector.d.ts +126 -0
- package/lib/epoch-collector.d.ts.map +1 -0
- package/lib/epoch-collector.js +243 -0
- package/lib/epoch-collector.js.map +1 -0
- package/lib/index.d.ts +62 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +143 -0
- package/lib/index.js.map +1 -0
- package/lib/preload-tools.d.ts +66 -0
- package/lib/preload-tools.d.ts.map +1 -0
- package/lib/preload-tools.js +101 -0
- package/lib/preload-tools.js.map +1 -0
- package/lib/registry.d.ts +49 -0
- package/lib/registry.d.ts.map +1 -0
- package/lib/registry.js +66 -0
- package/lib/registry.js.map +1 -0
- package/lib/resume-capture.d.ts +107 -0
- package/lib/resume-capture.d.ts.map +1 -0
- package/lib/resume-capture.js +232 -0
- package/lib/resume-capture.js.map +1 -0
- package/lib/sanitize-filter.d.ts +27 -0
- package/lib/sanitize-filter.d.ts.map +1 -0
- package/lib/sanitize-filter.js +95 -0
- package/lib/sanitize-filter.js.map +1 -0
- package/lib/strip-instructions.d.ts +47 -0
- package/lib/strip-instructions.d.ts.map +1 -0
- package/lib/strip-instructions.js +77 -0
- package/lib/strip-instructions.js.map +1 -0
- package/lib/suppress-settled.d.ts +45 -0
- package/lib/suppress-settled.d.ts.map +1 -0
- package/lib/suppress-settled.js +80 -0
- package/lib/suppress-settled.js.map +1 -0
- package/lib/tool.d.ts +49 -0
- package/lib/tool.d.ts.map +1 -0
- package/lib/tool.js +247 -0
- package/lib/tool.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace discovery of Claude Code `.claude/agents` definitions.
|
|
3
|
+
*
|
|
4
|
+
* The host process serves many workspaces at once, so discovery is keyed by
|
|
5
|
+
* the session's cwd (`cwdOf(agent)`) rather than the process cwd — a web host
|
|
6
|
+
* started from `~/.dsh` must still see `my-repo/.claude/agents`. Results are
|
|
7
|
+
* cached per root for the process lifetime: the registry does not watch the
|
|
8
|
+
* filesystem (v1), so editing an agent file takes effect on the next session
|
|
9
|
+
* for a workspace whose cache entry has not yet been created, and on process
|
|
10
|
+
* restart otherwise.
|
|
11
|
+
*
|
|
12
|
+
* @module @dsh-cc/subagent-task/registry
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentDefinition } from '@dsh-cc/claude-code-agents';
|
|
15
|
+
/** Options for the registry, mostly injectable seams for tests. */
|
|
16
|
+
export interface AgentRegistryOptions {
|
|
17
|
+
/** Override the user `.claude/agents` layer (hermetic tests). */
|
|
18
|
+
userDir?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A process-level cache of discovered agent definitions keyed by workspace
|
|
22
|
+
* root. Discovery runs lazily on first use of a root; the cached promise is
|
|
23
|
+
* shared so concurrent pre-steps for the same workspace scan once.
|
|
24
|
+
*/
|
|
25
|
+
export declare class AgentRegistry {
|
|
26
|
+
private readonly options;
|
|
27
|
+
private readonly cache;
|
|
28
|
+
constructor(options?: AgentRegistryOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Ensure the definitions for one workspace root are loaded (once).
|
|
31
|
+
* @param root - the session workspace (the project layer root).
|
|
32
|
+
* @returns the merged definition map (project shadows user), or an empty map.
|
|
33
|
+
*/
|
|
34
|
+
ensure(root: string): Promise<ReadonlyMap<string, AgentDefinition>>;
|
|
35
|
+
/**
|
|
36
|
+
* List every definition visible from one workspace root.
|
|
37
|
+
* @param root - the session workspace.
|
|
38
|
+
* @returns definitions sorted by name (project entries shadow user ones).
|
|
39
|
+
*/
|
|
40
|
+
list(root: string): Promise<readonly AgentDefinition[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve one definition by its frontmatter `name`.
|
|
43
|
+
* @param root - the session workspace.
|
|
44
|
+
* @param type - the requested `subagent_type`.
|
|
45
|
+
* @returns the definition, or undefined when the type is unknown here.
|
|
46
|
+
*/
|
|
47
|
+
resolve(root: string, type: string): Promise<AgentDefinition | undefined>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAEjE,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,qBAAa,aAAa;IAGZ,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmE;gBAE5D,OAAO,GAAE,oBAAyB;IAE/D;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAenE;;;;OAIG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC;IAK7D;;;;;OAKG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;CAIhF"}
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace discovery of Claude Code `.claude/agents` definitions.
|
|
3
|
+
*
|
|
4
|
+
* The host process serves many workspaces at once, so discovery is keyed by
|
|
5
|
+
* the session's cwd (`cwdOf(agent)`) rather than the process cwd — a web host
|
|
6
|
+
* started from `~/.dsh` must still see `my-repo/.claude/agents`. Results are
|
|
7
|
+
* cached per root for the process lifetime: the registry does not watch the
|
|
8
|
+
* filesystem (v1), so editing an agent file takes effect on the next session
|
|
9
|
+
* for a workspace whose cache entry has not yet been created, and on process
|
|
10
|
+
* restart otherwise.
|
|
11
|
+
*
|
|
12
|
+
* @module @dsh-cc/subagent-task/registry
|
|
13
|
+
*/
|
|
14
|
+
import { loadClaudeCodeAgents } from '@dsh-cc/claude-code-agents';
|
|
15
|
+
/**
|
|
16
|
+
* A process-level cache of discovered agent definitions keyed by workspace
|
|
17
|
+
* root. Discovery runs lazily on first use of a root; the cached promise is
|
|
18
|
+
* shared so concurrent pre-steps for the same workspace scan once.
|
|
19
|
+
*/
|
|
20
|
+
export class AgentRegistry {
|
|
21
|
+
options;
|
|
22
|
+
cache = new Map();
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ensure the definitions for one workspace root are loaded (once).
|
|
28
|
+
* @param root - the session workspace (the project layer root).
|
|
29
|
+
* @returns the merged definition map (project shadows user), or an empty map.
|
|
30
|
+
*/
|
|
31
|
+
ensure(root) {
|
|
32
|
+
let pending = this.cache.get(root);
|
|
33
|
+
if (pending === undefined) {
|
|
34
|
+
pending = loadClaudeCodeAgents(root, {
|
|
35
|
+
...this.options.userDir !== undefined ? { userDir: this.options.userDir } : {},
|
|
36
|
+
}).then(defs => {
|
|
37
|
+
const map = new Map();
|
|
38
|
+
for (const def of defs)
|
|
39
|
+
map.set(def.agentType, def);
|
|
40
|
+
return map;
|
|
41
|
+
});
|
|
42
|
+
this.cache.set(root, pending);
|
|
43
|
+
}
|
|
44
|
+
return pending;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* List every definition visible from one workspace root.
|
|
48
|
+
* @param root - the session workspace.
|
|
49
|
+
* @returns definitions sorted by name (project entries shadow user ones).
|
|
50
|
+
*/
|
|
51
|
+
async list(root) {
|
|
52
|
+
const map = await this.ensure(root);
|
|
53
|
+
return [...map.values()].sort((a, b) => a.agentType.localeCompare(b.agentType));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve one definition by its frontmatter `name`.
|
|
57
|
+
* @param root - the session workspace.
|
|
58
|
+
* @param type - the requested `subagent_type`.
|
|
59
|
+
* @returns the definition, or undefined when the type is unknown here.
|
|
60
|
+
*/
|
|
61
|
+
async resolve(root, type) {
|
|
62
|
+
const map = await this.ensure(root);
|
|
63
|
+
return map.get(type);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAA;AASjE;;;;GAIG;AACH,MAAM,OAAO,aAAa;IAGK;IAFZ,KAAK,GAAG,IAAI,GAAG,EAAyD,CAAA;IAEzF,YAA6B,UAAgC,EAAE;QAAlC,YAAO,GAAP,OAAO,CAA2B;IAAG,CAAC;IAEnE;;;;OAIG;IACH,MAAM,CAAC,IAAY;QACjB,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAClC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,GAAG,oBAAoB,CAAC,IAAI,EAAE;gBACnC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE;aAC/E,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBACb,MAAM,GAAG,GAAG,IAAI,GAAG,EAA2B,CAAA;gBAC9C,KAAK,MAAM,GAAG,IAAI,IAAI;oBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;gBACnD,OAAO,GAAG,CAAA;YACZ,CAAC,CAAC,CAAA;YACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAC/B,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACnC,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IACjF,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAY;QACtC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACnC,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC;CACF"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-time resume-pin capture (plan §4.3 + §4.5): build and write the pin
|
|
3
|
+
* for a continuable background child BEFORE `startContinuable`, and tombstone
|
|
4
|
+
* it when the creation throws.
|
|
5
|
+
*
|
|
6
|
+
* The capture flow lives in the task package (not resume-pins) because it is
|
|
7
|
+
* cordis-facing: it reads `ctx.llm.resolveCallConfig` for the §4.3 preflight
|
|
8
|
+
* and `ctx.logger` for the never-fail-the-spawn degradation warnings. Only
|
|
9
|
+
* pure helpers (`overlayRoute`, `probeWorkspace`) are exported for direct
|
|
10
|
+
* reuse and testing.
|
|
11
|
+
*
|
|
12
|
+
* Failure policy: a capture problem never fails the spawn. An unresolvable
|
|
13
|
+
* route degrades the pin to explicit-fields-only (`effective.complete:false` +
|
|
14
|
+
* a warning); an unwriteable pin skips capture entirely (a missing pin reads
|
|
15
|
+
* as a legacy/foreign child). Only the tombstone path rethrows nothing — the
|
|
16
|
+
* original `startContinuable` error is rethrown unchanged by the caller.
|
|
17
|
+
*
|
|
18
|
+
* @module @dsh-cc/subagent-task/resume-capture
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import type { AgentDefinition, ToolRestriction } from '@dsh-cc/claude-code-agents';
|
|
22
|
+
import type { DetailedRoute } from '@dsh-cc/model-aliases';
|
|
23
|
+
import { PinStore, type PinWorkspace } from '@dsh-cc/subagent-resume-pins';
|
|
24
|
+
/** The additive `apply(ctx, config)` option that arms spawn-time capture. */
|
|
25
|
+
export interface ResumePinsConfig {
|
|
26
|
+
/** Directory holding one `<childId>.json` pin file per child. */
|
|
27
|
+
readonly pinsRoot: string;
|
|
28
|
+
/** Test seam: inject a store instead of constructing one over `pinsRoot`. */
|
|
29
|
+
readonly store?: PinStore;
|
|
30
|
+
}
|
|
31
|
+
/** The parent's route fields the harness child-agent spread inherits from. */
|
|
32
|
+
export interface ParentRouteLike {
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly maxTokens?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The pre-preflight effective tuple: parent options overlaid with the alias
|
|
39
|
+
* route (§4.3). Mirrors the harness child-agent inheritance semantics
|
|
40
|
+
* (`child-agent.ts:73-79`): the parent's provider/model/maxTokens are spread
|
|
41
|
+
* conditionally, then the requested route's present fields win — `undefined`
|
|
42
|
+
* never shadows an inherited value.
|
|
43
|
+
*/
|
|
44
|
+
export declare function overlayRoute(parent: ParentRouteLike, route: DetailedRoute['route']): ParentRouteLike & {
|
|
45
|
+
readonly reasoningEffort?: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Workspace/worktree identity via ONE best-effort git probe. `cwd` is always
|
|
49
|
+
* present; on any probe failure (non-repo, git missing, timeout) the git
|
|
50
|
+
* fields fall back to the `'unknown'` sentinel — the pin schema has no
|
|
51
|
+
* optional fields, and a sentinel is parseable and visibly not an identity.
|
|
52
|
+
* gitDir/gitCommonDir are cwd-anchored ABSOLUTE paths so pins pin an
|
|
53
|
+
* identity, not a cwd-relative spelling.
|
|
54
|
+
*/
|
|
55
|
+
export declare function probeWorkspace(cwd: string): PinWorkspace;
|
|
56
|
+
/** Everything one pinned spawn contributes to the pin (§4.5). */
|
|
57
|
+
export interface CaptureInput {
|
|
58
|
+
readonly parentSessionId: string;
|
|
59
|
+
readonly label: string;
|
|
60
|
+
readonly childId: string;
|
|
61
|
+
/** A named definition (with discovery metadata); undefined for plain spawns. */
|
|
62
|
+
readonly definition?: AgentDefinition | undefined;
|
|
63
|
+
/** Atomic provenance resolution of the definition's `model` selector. */
|
|
64
|
+
readonly selector: DetailedRoute;
|
|
65
|
+
readonly parentRoute: ParentRouteLike;
|
|
66
|
+
/** The sanitized tool filter exactly as forwarded to the seam. */
|
|
67
|
+
readonly toolFilter?: ToolRestriction | undefined;
|
|
68
|
+
/** The session workspace cwd (workspace-identity probe root). */
|
|
69
|
+
readonly cwd: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Owns the write-before-spawn and tombstone-on-throw lifecycle for one
|
|
73
|
+
* mounted `resumePins` config. Never throws on capture trouble — a degraded
|
|
74
|
+
* or skipped pin is reported through `ctx.logger.warn` and the spawn proceeds.
|
|
75
|
+
*/
|
|
76
|
+
export declare class SpawnPinCapture {
|
|
77
|
+
private readonly ctx;
|
|
78
|
+
readonly store: PinStore;
|
|
79
|
+
constructor(ctx: Context, store: PinStore);
|
|
80
|
+
/** Preallocate the durable child id (it becomes the child's session id). */
|
|
81
|
+
preallocateChildId(): string;
|
|
82
|
+
/** The selector recorded when no route service is mounted: plain inherit. */
|
|
83
|
+
static inheritSelector(model: string | undefined): DetailedRoute;
|
|
84
|
+
/**
|
|
85
|
+
* Build the pin and write it before the spawn. The spawn is NEVER failed by
|
|
86
|
+
* capture trouble and capture is never silently skipped: resolves
|
|
87
|
+
* `undefined` on success, or the failure reason on a skipped capture — the
|
|
88
|
+
* caller MUST surface it as an explicit `captureWarning` line in the
|
|
89
|
+
* subagent_fork tool result ("this child will resume with legacy
|
|
90
|
+
* semantics"), so an unpinned launch is never silent.
|
|
91
|
+
*/
|
|
92
|
+
write(input: CaptureInput): Promise<string | undefined>;
|
|
93
|
+
/**
|
|
94
|
+
* Tombstone the pin after a failed `startContinuable`: delete the file; if
|
|
95
|
+
* the delete fails, rewrite it as blocked so it can never be mistaken for
|
|
96
|
+
* a live agent. Idempotent; never throws.
|
|
97
|
+
*/
|
|
98
|
+
tombstone(childId: string): Promise<void>;
|
|
99
|
+
private build;
|
|
100
|
+
/**
|
|
101
|
+
* §4.3 preflight: materialize the complete tuple with explicit nulls for
|
|
102
|
+
* absent effort/maxTokens. Failure degrades to the explicit overlay fields
|
|
103
|
+
* with `complete:false` plus a warning — never a spawn failure.
|
|
104
|
+
*/
|
|
105
|
+
private preflight;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=resume-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-capture.d.ts","sourceRoot":"","sources":["../src/resume-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EACL,QAAQ,EAMR,KAAK,YAAY,EAGlB,MAAM,8BAA8B,CAAA;AAErC,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAA;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAC5B,eAAe,GAAG;IAAE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CASzD;AAuBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAuBxD;AAYD,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACjD,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAA;IACrC,kEAAkE;IAClE,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACjD,iEAAiE;IACjE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,QAAQ;gBADP,GAAG,EAAE,OAAO,EACpB,KAAK,EAAE,QAAQ;IAG1B,4EAA4E;IAC5E,kBAAkB,IAAI,MAAM;IAI5B,6EAA6E;IAC7E,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa;IAIhE;;;;;;;OAOG;IACG,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAW7D;;;;OAIG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAkBjC,KAAK;IA2CnB;;;;OAIG;YACW,SAAS;CAgCxB"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-time resume-pin capture (plan §4.3 + §4.5): build and write the pin
|
|
3
|
+
* for a continuable background child BEFORE `startContinuable`, and tombstone
|
|
4
|
+
* it when the creation throws.
|
|
5
|
+
*
|
|
6
|
+
* The capture flow lives in the task package (not resume-pins) because it is
|
|
7
|
+
* cordis-facing: it reads `ctx.llm.resolveCallConfig` for the §4.3 preflight
|
|
8
|
+
* and `ctx.logger` for the never-fail-the-spawn degradation warnings. Only
|
|
9
|
+
* pure helpers (`overlayRoute`, `probeWorkspace`) are exported for direct
|
|
10
|
+
* reuse and testing.
|
|
11
|
+
*
|
|
12
|
+
* Failure policy: a capture problem never fails the spawn. An unresolvable
|
|
13
|
+
* route degrades the pin to explicit-fields-only (`effective.complete:false` +
|
|
14
|
+
* a warning); an unwriteable pin skips capture entirely (a missing pin reads
|
|
15
|
+
* as a legacy/foreign child). Only the tombstone path rethrows nothing — the
|
|
16
|
+
* original `startContinuable` error is rethrown unchanged by the caller.
|
|
17
|
+
*
|
|
18
|
+
* @module @dsh-cc/subagent-task/resume-capture
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { isAbsolute, join } from 'node:path';
|
|
23
|
+
import { realpathSync } from 'node:fs';
|
|
24
|
+
import { definitionFingerprint, personaHash, } from '@dsh-cc/subagent-resume-pins';
|
|
25
|
+
/**
|
|
26
|
+
* The pre-preflight effective tuple: parent options overlaid with the alias
|
|
27
|
+
* route (§4.3). Mirrors the harness child-agent inheritance semantics
|
|
28
|
+
* (`child-agent.ts:73-79`): the parent's provider/model/maxTokens are spread
|
|
29
|
+
* conditionally, then the requested route's present fields win — `undefined`
|
|
30
|
+
* never shadows an inherited value.
|
|
31
|
+
*/
|
|
32
|
+
export function overlayRoute(parent, route) {
|
|
33
|
+
return {
|
|
34
|
+
...parent.provider !== undefined ? { provider: parent.provider } : {},
|
|
35
|
+
...parent.model !== undefined ? { model: parent.model } : {},
|
|
36
|
+
...parent.maxTokens !== undefined ? { maxTokens: parent.maxTokens } : {},
|
|
37
|
+
...route?.provider !== undefined ? { provider: route.provider } : {},
|
|
38
|
+
...route?.model !== undefined ? { model: route.model } : {},
|
|
39
|
+
...route?.reasoningEffort !== undefined ? { reasoningEffort: route.reasoningEffort } : {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The git probe timeout; a hung git must never delay a spawn. */
|
|
43
|
+
const GIT_PROBE_TIMEOUT_MS = 2_000;
|
|
44
|
+
/**
|
|
45
|
+
* Normalize one `git rev-parse` output to a cwd-anchored absolute path (git
|
|
46
|
+
* prints repository-relative values like `.git`), realpath'ed when cheap.
|
|
47
|
+
* Residual limit (documented): two DIFFERENT standalone repos initialized
|
|
48
|
+
* sequentially at the same path normalize to the same path strings, so that
|
|
49
|
+
* replacement is not detectable — the worktree↔standalone flip and cwd moves
|
|
50
|
+
* ARE (the comparison is the --git-dir path pair, per the implemented rule).
|
|
51
|
+
*/
|
|
52
|
+
function normalizeGitPath(cwd, value) {
|
|
53
|
+
if (value.length === 0)
|
|
54
|
+
return 'unknown';
|
|
55
|
+
const absolute = isAbsolute(value) ? value : join(cwd, value);
|
|
56
|
+
try {
|
|
57
|
+
return realpathSync(absolute);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return absolute;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Workspace/worktree identity via ONE best-effort git probe. `cwd` is always
|
|
65
|
+
* present; on any probe failure (non-repo, git missing, timeout) the git
|
|
66
|
+
* fields fall back to the `'unknown'` sentinel — the pin schema has no
|
|
67
|
+
* optional fields, and a sentinel is parseable and visibly not an identity.
|
|
68
|
+
* gitDir/gitCommonDir are cwd-anchored ABSOLUTE paths so pins pin an
|
|
69
|
+
* identity, not a cwd-relative spelling.
|
|
70
|
+
*/
|
|
71
|
+
export function probeWorkspace(cwd) {
|
|
72
|
+
const fallback = { cwd, gitDir: 'unknown', gitCommonDir: 'unknown', branch: 'unknown' };
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = spawnSync('git', ['-C', cwd, 'rev-parse', '--git-dir', '--git-common-dir', '--abbrev-ref', 'HEAD'], { timeout: GIT_PROBE_TIMEOUT_MS, encoding: 'utf8' });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
if (result.status !== 0 || typeof result.stdout !== 'string')
|
|
81
|
+
return fallback;
|
|
82
|
+
const [gitDir, gitCommonDir, branch] = result.stdout.trim().split('\n');
|
|
83
|
+
if (gitDir === undefined || gitCommonDir === undefined || branch === undefined || branch.length === 0) {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
cwd,
|
|
88
|
+
gitDir: normalizeGitPath(cwd, gitDir),
|
|
89
|
+
gitCommonDir: normalizeGitPath(cwd, gitCommonDir),
|
|
90
|
+
branch,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Owns the write-before-spawn and tombstone-on-throw lifecycle for one
|
|
95
|
+
* mounted `resumePins` config. Never throws on capture trouble — a degraded
|
|
96
|
+
* or skipped pin is reported through `ctx.logger.warn` and the spawn proceeds.
|
|
97
|
+
*/
|
|
98
|
+
export class SpawnPinCapture {
|
|
99
|
+
ctx;
|
|
100
|
+
store;
|
|
101
|
+
constructor(ctx, store) {
|
|
102
|
+
this.ctx = ctx;
|
|
103
|
+
this.store = store;
|
|
104
|
+
}
|
|
105
|
+
/** Preallocate the durable child id (it becomes the child's session id). */
|
|
106
|
+
preallocateChildId() {
|
|
107
|
+
return randomUUID();
|
|
108
|
+
}
|
|
109
|
+
/** The selector recorded when no route service is mounted: plain inherit. */
|
|
110
|
+
static inheritSelector(model) {
|
|
111
|
+
return { selector: model, via: 'inherit', route: undefined };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the pin and write it before the spawn. The spawn is NEVER failed by
|
|
115
|
+
* capture trouble and capture is never silently skipped: resolves
|
|
116
|
+
* `undefined` on success, or the failure reason on a skipped capture — the
|
|
117
|
+
* caller MUST surface it as an explicit `captureWarning` line in the
|
|
118
|
+
* subagent_fork tool result ("this child will resume with legacy
|
|
119
|
+
* semantics"), so an unpinned launch is never silent.
|
|
120
|
+
*/
|
|
121
|
+
async write(input) {
|
|
122
|
+
try {
|
|
123
|
+
this.store.write(await this.build(input));
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const reason = error.message;
|
|
128
|
+
this.ctx.logger.warn(`resume pin capture failed (spawn proceeds unpinned): ${reason}`);
|
|
129
|
+
return reason;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Tombstone the pin after a failed `startContinuable`: delete the file; if
|
|
134
|
+
* the delete fails, rewrite it as blocked so it can never be mistaken for
|
|
135
|
+
* a live agent. Idempotent; never throws.
|
|
136
|
+
*/
|
|
137
|
+
async tombstone(childId) {
|
|
138
|
+
try {
|
|
139
|
+
this.store.remove(childId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fall through to the blocked rewrite.
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
this.store.update(childId, (draft) => {
|
|
147
|
+
draft.resume = { state: 'blocked', reason: 'spawn-aborted' };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.ctx.logger.warn(`resume pin tombstone failed for child ${childId}: ${error.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async build(input) {
|
|
155
|
+
const route = input.selector.route;
|
|
156
|
+
const overlay = overlayRoute(input.parentRoute, route);
|
|
157
|
+
if (overlay.provider === undefined || overlay.model === undefined) {
|
|
158
|
+
throw new Error('no explicit provider/model route to pin');
|
|
159
|
+
}
|
|
160
|
+
const effective = await this.preflight(overlay);
|
|
161
|
+
const definition = input.definition === undefined
|
|
162
|
+
? { kind: 'plain' }
|
|
163
|
+
: {
|
|
164
|
+
kind: 'named',
|
|
165
|
+
agentType: input.definition.agentType,
|
|
166
|
+
source: input.definition.source,
|
|
167
|
+
fingerprint: definitionFingerprint(input.definition),
|
|
168
|
+
personaHash: personaHash(input.definition.systemPrompt),
|
|
169
|
+
// Gate-time re-fingerprinting needs the discovery location (§4.4).
|
|
170
|
+
baseDir: input.definition.baseDir,
|
|
171
|
+
filename: input.definition.filename,
|
|
172
|
+
};
|
|
173
|
+
const modelSelector = {
|
|
174
|
+
raw: input.selector.selector ?? 'inherit',
|
|
175
|
+
via: input.selector.via,
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
version: 1,
|
|
179
|
+
childId: input.childId,
|
|
180
|
+
parentSessionId: input.parentSessionId,
|
|
181
|
+
label: input.label,
|
|
182
|
+
mode: 'continuable-background',
|
|
183
|
+
createdAt: new Date().toISOString(),
|
|
184
|
+
definition,
|
|
185
|
+
modelSelector,
|
|
186
|
+
effective,
|
|
187
|
+
toolFilter: {
|
|
188
|
+
allow: [...(input.toolFilter?.allow ?? [])],
|
|
189
|
+
deny: [...(input.toolFilter?.deny ?? [])],
|
|
190
|
+
},
|
|
191
|
+
...input.definition?.maxTurns !== undefined ? { maxTurns: input.definition.maxTurns } : {},
|
|
192
|
+
workspace: probeWorkspace(input.cwd),
|
|
193
|
+
resume: { state: 'ok' },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* §4.3 preflight: materialize the complete tuple with explicit nulls for
|
|
198
|
+
* absent effort/maxTokens. Failure degrades to the explicit overlay fields
|
|
199
|
+
* with `complete:false` plus a warning — never a spawn failure.
|
|
200
|
+
*/
|
|
201
|
+
async preflight(overlay) {
|
|
202
|
+
const llm = this.ctx.get('llm');
|
|
203
|
+
try {
|
|
204
|
+
if (llm === undefined)
|
|
205
|
+
throw new Error('no llm service for the preflight');
|
|
206
|
+
const config = await llm.resolveCallConfig({
|
|
207
|
+
provider: overlay.provider,
|
|
208
|
+
model: overlay.model,
|
|
209
|
+
...(overlay.maxTokens !== undefined ? { maxTokens: overlay.maxTokens } : {}),
|
|
210
|
+
...(overlay.reasoningEffort !== undefined ? { reasoningEffort: overlay.reasoningEffort } : {}),
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
provider: config.provider,
|
|
214
|
+
model: config.model,
|
|
215
|
+
reasoningEffort: config.reasoningEffort ?? null,
|
|
216
|
+
maxTokens: config.maxTokens ?? null,
|
|
217
|
+
complete: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
this.ctx.logger.warn('resume pin degraded: route not preflightable; only explicit options pinned');
|
|
222
|
+
return {
|
|
223
|
+
provider: overlay.provider,
|
|
224
|
+
model: overlay.model,
|
|
225
|
+
reasoningEffort: overlay.reasoningEffort ?? null,
|
|
226
|
+
maxTokens: overlay.maxTokens ?? null,
|
|
227
|
+
complete: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=resume-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume-capture.js","sourceRoot":"","sources":["../src/resume-capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a Task child's tool restriction against the live restrictable-name
|
|
3
|
+
* set: MCP wildcards expand, unknown names drop, an emptied allow-list is
|
|
4
|
+
* deny-all, and ToolSearch is auto-injected when the child holds MCP names.
|
|
5
|
+
*
|
|
6
|
+
* @module @dsh-cc/subagent-task/sanitize-filter
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolRestriction } from '@dsh-cc/claude-code-agents';
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a definition's tool restriction against the LIVE set of names the
|
|
11
|
+
* tools registry knows (registered or reserved — `ctx.tools.view(callingAgent)
|
|
12
|
+
* .restrictableNames`, read at execute time so deferred MCP reservations on
|
|
13
|
+
* the standing-scope layer are included).
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - A name survives only when the registry knows it. Everything else is
|
|
17
|
+
* dropped with a warning; there is no static legal-names set, so mounted
|
|
18
|
+
* MCP tools and any future registered row are accepted without code churn.
|
|
19
|
+
* - If the filter carried an `allow` list and any kept allow name is an MCP
|
|
20
|
+
* tool while `ToolSearch` is itself restrictable, `ToolSearch` is appended
|
|
21
|
+
* (deduped): the child otherwise holds MCP names with no load path.
|
|
22
|
+
* - If the filter carried an `allow` list and sanitization left nothing, the
|
|
23
|
+
* result is `{ allow: [] }` — omitting `allow` would WIDEN the child to
|
|
24
|
+
* every tool, so an emptied allow-list is pinned as deny-all, loudly.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sanitizeToolFilter(filter: ToolRestriction, warn: (m: string) => void, knownNames: ReadonlySet<string>): ToolRestriction;
|
|
27
|
+
//# sourceMappingURL=sanitize-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-filter.d.ts","sourceRoot":"","sources":["../src/sanitize-filter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AA4CjE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,EACzB,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,GAC9B,eAAe,CA+BjB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize a Task child's tool restriction against the live restrictable-name
|
|
3
|
+
* set: MCP wildcards expand, unknown names drop, an emptied allow-list is
|
|
4
|
+
* deny-all, and ToolSearch is auto-injected when the child holds MCP names.
|
|
5
|
+
*
|
|
6
|
+
* @module @dsh-cc/subagent-task/sanitize-filter
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The MCP public-name prefix every bridged MCP tool carries on `ctx.tools`.
|
|
10
|
+
*/
|
|
11
|
+
const MCP_PUBLIC_PREFIX = 'mcp__';
|
|
12
|
+
/**
|
|
13
|
+
* Expand one raw filter entry into the concrete names it asks for.
|
|
14
|
+
*
|
|
15
|
+
* - Anything not MCP-qualified passes through untouched (it is then gated by
|
|
16
|
+
* the `knownNames` check).
|
|
17
|
+
* - A bare `mcp__` (no server segment) is dropped with a loud warning — it
|
|
18
|
+
* can never name a mounted tool.
|
|
19
|
+
* - `mcp__<server>` (no third segment) and `mcp__<server>__*` expand to every
|
|
20
|
+
* known MCP tool of that server (`mcp__<server>__` prefix), so frontmatter
|
|
21
|
+
* survives servers publishing new tools without a hash-suffix dance.
|
|
22
|
+
* - An exact `mcp__<server>__<tool>` passes through as written (the caller
|
|
23
|
+
* must use the public name, including any identity-hash suffix).
|
|
24
|
+
*/
|
|
25
|
+
function expandFilterName(rawName, knownNames, warn) {
|
|
26
|
+
if (!rawName.startsWith(MCP_PUBLIC_PREFIX))
|
|
27
|
+
return [rawName];
|
|
28
|
+
const rest = rawName.slice(MCP_PUBLIC_PREFIX.length);
|
|
29
|
+
if (rest.length === 0) {
|
|
30
|
+
warn('cc-task: invalid MCP wildcard "mcp__" in a subagent toolFilter — expected mcp__<server> or mcp__<server>__<tool>');
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const server = rest.endsWith('__*')
|
|
34
|
+
? rest.slice(0, -'__*'.length)
|
|
35
|
+
: rest.includes('__')
|
|
36
|
+
? undefined
|
|
37
|
+
: rest;
|
|
38
|
+
if (server === undefined)
|
|
39
|
+
return [rawName];
|
|
40
|
+
if (server.length === 0) {
|
|
41
|
+
warn(`cc-task: invalid MCP wildcard "${rawName}" in a subagent toolFilter — expected mcp__<server> or mcp__<server>__<tool>`);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return [...knownNames].filter(name => name.startsWith(`${MCP_PUBLIC_PREFIX}${server}__`));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sanitize a definition's tool restriction against the LIVE set of names the
|
|
48
|
+
* tools registry knows (registered or reserved — `ctx.tools.view(callingAgent)
|
|
49
|
+
* .restrictableNames`, read at execute time so deferred MCP reservations on
|
|
50
|
+
* the standing-scope layer are included).
|
|
51
|
+
*
|
|
52
|
+
* Rules:
|
|
53
|
+
* - A name survives only when the registry knows it. Everything else is
|
|
54
|
+
* dropped with a warning; there is no static legal-names set, so mounted
|
|
55
|
+
* MCP tools and any future registered row are accepted without code churn.
|
|
56
|
+
* - If the filter carried an `allow` list and any kept allow name is an MCP
|
|
57
|
+
* tool while `ToolSearch` is itself restrictable, `ToolSearch` is appended
|
|
58
|
+
* (deduped): the child otherwise holds MCP names with no load path.
|
|
59
|
+
* - If the filter carried an `allow` list and sanitization left nothing, the
|
|
60
|
+
* result is `{ allow: [] }` — omitting `allow` would WIDEN the child to
|
|
61
|
+
* every tool, so an emptied allow-list is pinned as deny-all, loudly.
|
|
62
|
+
*/
|
|
63
|
+
export function sanitizeToolFilter(filter, warn, knownNames) {
|
|
64
|
+
const clean = (names) => {
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const rawName of names) {
|
|
67
|
+
for (const expanded of expandFilterName(rawName, knownNames, warn)) {
|
|
68
|
+
if (knownNames.has(expanded)) {
|
|
69
|
+
if (!out.includes(expanded))
|
|
70
|
+
out.push(expanded);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
warn(`cc-task: dropping unknown tool name "${expanded}" from a subagent toolFilter`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
const hadAllow = filter.allow !== undefined;
|
|
80
|
+
const allow = filter.allow !== undefined ? clean(filter.allow) : undefined;
|
|
81
|
+
const deny = filter.deny !== undefined ? clean(filter.deny) : undefined;
|
|
82
|
+
if (hadAllow && allow !== undefined && allow.length > 0 && allow.some(name => name.startsWith(MCP_PUBLIC_PREFIX))
|
|
83
|
+
&& knownNames.has('ToolSearch') && !allow.includes('ToolSearch')) {
|
|
84
|
+
allow.push('ToolSearch');
|
|
85
|
+
}
|
|
86
|
+
if (hadAllow && (allow === undefined || allow.length === 0)) {
|
|
87
|
+
warn('cc-task: a subagent toolFilter allow-list matched no mounted tools '
|
|
88
|
+
+ `(originals: ${(filter.allow ?? []).join(', ')}); the child will run with zero tools`);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...(hadAllow ? { allow: allow ?? [] } : {}),
|
|
92
|
+
...(deny !== undefined && deny.length > 0 ? { deny } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=sanitize-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-filter.js","sourceRoot":"","sources":["../src/sanitize-filter.ts"],"names":[],"mappings":"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"}
|