@stigmer/runner 3.12.4 → 3.12.5
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
- package/dist/activities/execute-cursor/index.d.ts +10 -1
- package/dist/activities/execute-cursor/index.js +47 -29
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
- package/dist/activities/execute-cursor/skill-resolver.js +5 -108
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +57 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +17 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/path-normalization.d.ts +49 -49
- package/dist/middleware/path-normalization.js +73 -84
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/middleware/types.d.ts +3 -3
- package/dist/runner-manager.d.ts +1 -1
- package/dist/runner-manager.js +22 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/args-preview.d.ts +8 -0
- package/dist/shared/args-preview.js +14 -3
- package/dist/shared/args-preview.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -1
- package/dist/shared/artifact-storage.js +10 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/declared-preferences.d.ts +47 -0
- package/dist/shared/declared-preferences.js +64 -0
- package/dist/shared/declared-preferences.js.map +1 -0
- package/dist/shared/mcp-enabled-tools.d.ts +2 -3
- package/dist/shared/mcp-enabled-tools.js +2 -3
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +20 -15
- package/dist/shared/mcp-resolver.js +11 -12
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/placeholder-resolver.d.ts +9 -2
- package/dist/shared/placeholder-resolver.js +9 -2
- package/dist/shared/placeholder-resolver.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -46
- package/dist/shared/plan-mode-permissions.js +27 -55
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/skill-mount.d.ts +89 -0
- package/dist/shared/skill-mount.js +142 -0
- package/dist/shared/skill-mount.js.map +1 -0
- package/dist/shared/skill-writer.d.ts +28 -26
- package/dist/shared/skill-writer.js +79 -102
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/worker-shutdown.d.ts +67 -0
- package/dist/shared/worker-shutdown.js +79 -0
- package/dist/shared/worker-shutdown.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +3 -2
- package/dist/workflow-engine/loader.js +38 -1
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +23 -2
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/package.json +15 -11
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
- package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
- package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
- package/src/activities/execute-cursor/index.ts +63 -35
- package/src/activities/execute-cursor/prompt-builder.ts +29 -2
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
- package/src/activities/execute-cursor/skill-resolver.ts +10 -137
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
- package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
- package/src/activities/execute-deep-agent/index.ts +57 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
- package/src/activities/execute-deep-agent/setup.ts +21 -19
- package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
- package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
- package/src/index.ts +1 -1
- package/src/middleware/__tests__/path-normalization.test.ts +32 -32
- package/src/middleware/path-normalization.ts +78 -90
- package/src/middleware/types.ts +3 -3
- package/src/runner-manager.ts +26 -16
- package/src/runner.ts +14 -0
- package/src/shared/__tests__/artifact-storage.test.ts +28 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
- package/src/shared/__tests__/declared-preferences.test.ts +109 -0
- package/src/shared/__tests__/foundry-seam.test.ts +5 -5
- package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
- package/src/shared/__tests__/skill-mount.test.ts +238 -0
- package/src/shared/__tests__/skill-writer.test.ts +151 -128
- package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
- package/src/shared/__tests__/vertex-seam.test.ts +5 -5
- package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
- package/src/shared/args-preview.ts +17 -3
- package/src/shared/artifact-storage.ts +20 -1
- package/src/shared/declared-preferences.ts +84 -0
- package/src/shared/mcp-enabled-tools.ts +2 -3
- package/src/shared/mcp-resolver.ts +20 -20
- package/src/shared/placeholder-resolver.ts +9 -2
- package/src/shared/plan-mode-permissions.ts +27 -58
- package/src/shared/skill-mount.ts +179 -0
- package/src/shared/skill-writer.ts +96 -130
- package/src/shared/worker-shutdown.ts +99 -0
- package/src/shared/workspace/types.ts +3 -2
- package/src/workflow-engine/__tests__/loader.test.ts +51 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
- package/src/workflow-engine/loader.ts +46 -1
- package/src/workflow-engine/tasks/human-input.ts +33 -5
- package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
- package/dist/activities/execute-cursor/connect-backfill.js +0 -27
- package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
- package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
- package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
- package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
- package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
- package/src/activities/execute-cursor/connect-backfill.ts +0 -52
- package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
- package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill mount mechanics — the ONE cache shape shared by both harnesses.
|
|
3
|
+
*
|
|
4
|
+
* A skill "mount" is the on-disk materialization of a skill inside the
|
|
5
|
+
* session's platform directory: `{platformDir}/skills/{name}/` holding
|
|
6
|
+
* SKILL.md plus the artifact's supporting files. Both harnesses write to
|
|
7
|
+
* that same physical location — the Cursor harness directly, the native
|
|
8
|
+
* (deep-agent) harness through the `.stigmer/` virtual namespace that
|
|
9
|
+
* LocalWorkspaceBackend routes there — so they MUST share one cache shape
|
|
10
|
+
* (issues #337/#672). This module owns that shape:
|
|
11
|
+
*
|
|
12
|
+
* - The mount is cached by the skill's content-addressed
|
|
13
|
+
* `status.version_hash`: metadata is still fetched every execution
|
|
14
|
+
* (that keeps latest-version freshness — push a skill update and the
|
|
15
|
+
* very next message picks it up), but the artifact download and file
|
|
16
|
+
* rewrite are skipped when the mounted content's hash already matches.
|
|
17
|
+
* - Remounts rebuild the directory from scratch, so files deleted between
|
|
18
|
+
* skill versions never linger in the mount.
|
|
19
|
+
* - Crash-safe by ordering: the marker is stamped LAST, after every file
|
|
20
|
+
* of the mount landed — a crash mid-write leaves no marker, so the next
|
|
21
|
+
* execution remounts instead of trusting a partial tree.
|
|
22
|
+
*
|
|
23
|
+
* Extracted from execute-cursor/skill-resolver.ts (PR #682) when the
|
|
24
|
+
* deep-agent path adopted the same cache (issue #337). Orchestration —
|
|
25
|
+
* which skills to mount, prompt metadata, degradation logging — stays
|
|
26
|
+
* with each harness; only the per-skill-directory mechanics live here.
|
|
27
|
+
*/
|
|
28
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
29
|
+
import { join, dirname, extname, resolve } from "node:path";
|
|
30
|
+
import { ConnectError, Code } from "@connectrpc/connect";
|
|
31
|
+
import { extractZipFileEntries } from "./zip-extract.js";
|
|
32
|
+
/** Subdirectory of the platform dir where skill mounts live. */
|
|
33
|
+
export const SKILLS_SUBDIR = "skills";
|
|
34
|
+
/**
|
|
35
|
+
* Marker recording what a skill's mount directory currently holds. Written
|
|
36
|
+
* LAST, after every file of the mount landed — a crash mid-write leaves no
|
|
37
|
+
* marker, so the next execution remounts instead of trusting a partial tree.
|
|
38
|
+
*/
|
|
39
|
+
export const MOUNT_MARKER_FILE = ".stigmer-mount.json";
|
|
40
|
+
/**
|
|
41
|
+
* File extensions written with the executable bit set. The ZIP layer is
|
|
42
|
+
* deliberately mode-blind (path + bytes only, see zip-extract.ts), so
|
|
43
|
+
* extension is the only signal available for `./script.sh`-style
|
|
44
|
+
* invocation to work out of the mount.
|
|
45
|
+
*/
|
|
46
|
+
const SCRIPT_EXTENSIONS = new Set([".sh", ".py", ".js", ".ts", ".rb", ".pl"]);
|
|
47
|
+
/**
|
|
48
|
+
* Whether the mount at `skillDir` already holds this version's content.
|
|
49
|
+
*
|
|
50
|
+
* Fresh means: the marker's hash matches AND the mount isn't a degraded
|
|
51
|
+
* SKILL.md-only fallback when the skill does carry an artifact. Any read or
|
|
52
|
+
* parse failure counts as stale — the remount is the safe default.
|
|
53
|
+
*/
|
|
54
|
+
export async function mountIsFresh(skillDir, versionHash, wantsArtifact) {
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readFile(join(skillDir, MOUNT_MARKER_FILE), "utf-8");
|
|
57
|
+
const marker = JSON.parse(raw);
|
|
58
|
+
return marker.versionHash === versionHash && (marker.artifactMounted === true || !wantsArtifact);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Download a skill artifact's ZIP bytes, transfer lane first (#675).
|
|
66
|
+
*
|
|
67
|
+
* The URL lane (getArtifactDownloadUrl → HTTP GET) carries any valid skill
|
|
68
|
+
* size; the unary getArtifact response is capped by the server's 10MB gRPC
|
|
69
|
+
* message limit. Servers that predate the lane (and cloud until its sibling
|
|
70
|
+
* lands) answer the mint with UNIMPLEMENTED — those fall back to the unary
|
|
71
|
+
* path, which behaves exactly as before for ≤10MB artifacts.
|
|
72
|
+
*
|
|
73
|
+
* Runs only on a mount-cache miss (the hash-keyed marker above) — a hit
|
|
74
|
+
* skips the transfer entirely, whichever lane would have carried it.
|
|
75
|
+
*/
|
|
76
|
+
export async function downloadArtifact(client, artifactStorageKey) {
|
|
77
|
+
let minted;
|
|
78
|
+
try {
|
|
79
|
+
minted = await client.getSkillArtifactDownloadUrl(artifactStorageKey);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof ConnectError && err.code === Code.Unimplemented) {
|
|
83
|
+
const resp = await client.getSkillArtifact(artifactStorageKey);
|
|
84
|
+
return resp.artifact && resp.artifact.length > 0 ? resp.artifact : undefined;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
const resp = await fetch(minted.url);
|
|
89
|
+
if (!resp.ok) {
|
|
90
|
+
throw new Error(`artifact fetch failed: HTTP ${resp.status} from ${minted.url}`);
|
|
91
|
+
}
|
|
92
|
+
const bytes = new Uint8Array(await resp.arrayBuffer());
|
|
93
|
+
if (minted.sizeBytes > 0n && BigInt(bytes.length) !== minted.sizeBytes) {
|
|
94
|
+
throw new Error(`artifact fetch truncated: got ${bytes.length} bytes, expected ${minted.sizeBytes}`);
|
|
95
|
+
}
|
|
96
|
+
return bytes.length > 0 ? bytes : undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* (Re)write a skill's mount directory from scratch.
|
|
100
|
+
*
|
|
101
|
+
* The directory is removed first so files deleted between versions don't
|
|
102
|
+
* linger in the mount, then SKILL.md and the artifact files are written, and
|
|
103
|
+
* the marker is stamped LAST (see MOUNT_MARKER_FILE for the crash-safety
|
|
104
|
+
* contract). SKILL.md always comes from `spec.skillMd` — the server's
|
|
105
|
+
* authoritative copy, which the push pipeline guarantees is non-empty
|
|
106
|
+
* (push.go hard-fails a ZIP without an extractable SKILL.md) — never from
|
|
107
|
+
* the zip; both the zip's SKILL.md and any stray marker-named entry are
|
|
108
|
+
* excluded from extraction so the mount's ownership of those two files is
|
|
109
|
+
* unconditional.
|
|
110
|
+
*
|
|
111
|
+
* Every extracted entry must resolve inside `skillDir`. The server already
|
|
112
|
+
* rejects traversal at push (google/safearchive), but this runner-side
|
|
113
|
+
* check is kept as defense-in-depth: it is the guard the deep-agent path
|
|
114
|
+
* used to get from LocalWorkspaceBackend's platform-path routing, preserved
|
|
115
|
+
* here when skill writes moved to direct fs.
|
|
116
|
+
*/
|
|
117
|
+
export async function writeSkillMount(skill, skillDir, artifactBytes) {
|
|
118
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
119
|
+
await mkdir(skillDir, { recursive: true });
|
|
120
|
+
await writeFile(join(skillDir, "SKILL.md"), skill.spec.skillMd, "utf-8");
|
|
121
|
+
const artifactMounted = artifactBytes !== undefined && artifactBytes.length > 0;
|
|
122
|
+
if (artifactMounted) {
|
|
123
|
+
const normalizedSkillDir = resolve(skillDir);
|
|
124
|
+
const entries = await extractZipFileEntries(artifactBytes, { exclude: ["SKILL.md", MOUNT_MARKER_FILE] });
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const filePath = resolve(normalizedSkillDir, entry.path);
|
|
127
|
+
if (filePath !== normalizedSkillDir && !filePath.startsWith(normalizedSkillDir + "/")) {
|
|
128
|
+
throw new Error(`skill artifact entry escapes its mount directory: '${entry.path}'`);
|
|
129
|
+
}
|
|
130
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
131
|
+
await writeFile(filePath, entry.content, {
|
|
132
|
+
mode: SCRIPT_EXTENSIONS.has(extname(entry.path)) ? 0o755 : 0o644,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const versionHash = skill.status?.versionHash ?? "";
|
|
137
|
+
if (versionHash !== "") {
|
|
138
|
+
const marker = { versionHash, artifactMounted };
|
|
139
|
+
await writeFile(join(skillDir, MOUNT_MARKER_FILE), JSON.stringify(marker), "utf-8");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=skill-mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-mount.js","sourceRoot":"","sources":["../../src/shared/skill-mount.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill
|
|
2
|
+
* Skill mounting and prompt generation for the deep-agent execution path.
|
|
3
3
|
*
|
|
4
4
|
* Follows the Agent Skills specification progressive disclosure model:
|
|
5
5
|
*
|
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
* 3. Resources (on demand) — scripts, references, and assets are
|
|
11
11
|
* loaded by the agent only when required.
|
|
12
12
|
*
|
|
13
|
-
* Skills live
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Skills physically live in the session's platform directory
|
|
14
|
+
* (`{platformDir}/skills/{name}/` — the SAME location the Cursor harness
|
|
15
|
+
* mounts into), and the agent sees them as `.stigmer/skills/{name}/`
|
|
16
|
+
* through the per-turn workspace symlink (see workspace/stigmer-link.ts).
|
|
17
|
+
* Returned paths are the agent-visible `.stigmer/…` form.
|
|
18
|
+
*
|
|
19
|
+
* Mounts are cached by the skill's content-addressed version hash via the
|
|
20
|
+
* shared skill-mount mechanics (issue #337, mirroring the Cursor harness's
|
|
21
|
+
* #672 fix): metadata is fetched every execution — a pushed skill update
|
|
22
|
+
* still lands on the very next message — but an unchanged skill skips the
|
|
23
|
+
* artifact download and rewrite entirely.
|
|
16
24
|
*/
|
|
17
|
-
import type { WorkspaceBackend } from "./workspace/types.js";
|
|
18
25
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
19
26
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
20
27
|
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
@@ -33,24 +40,24 @@ export declare function mergeSkillRefs(agentRefs: readonly ApiResourceReference[
|
|
|
33
40
|
*/
|
|
34
41
|
export declare function fetchSkillsByRefs(client: StigmerClient, refs: readonly ApiResourceReference[]): Promise<Skill[]>;
|
|
35
42
|
/**
|
|
36
|
-
*
|
|
37
|
-
* skill
|
|
43
|
+
* Mount skills into the session's platform directory, downloading each
|
|
44
|
+
* skill's artifact only on a version-hash cache miss (see skill-mount.ts
|
|
45
|
+
* for the marker mechanics). Returns a map of skill-id -> agent-visible
|
|
46
|
+
* directory path.
|
|
38
47
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* Check workspace integrity for resume fast-path.
|
|
51
|
-
* Returns true if the sentinel SKILL.md file exists for the first skill.
|
|
48
|
+
* The returned map is a NAMING function, not a success record: every skill
|
|
49
|
+
* gets its path entry so prompt generation stays total, while mount
|
|
50
|
+
* failures degrade that one skill (loud log, no throw) — one broken skill
|
|
51
|
+
* must never kill the run.
|
|
52
|
+
*
|
|
53
|
+
* Skills mount in parallel (each owns an independent directory), EXCEPT
|
|
54
|
+
* when two skills resolve to the same directory name (same `spec.name`
|
|
55
|
+
* from different orgs): concurrent remove-and-rewrite passes on one
|
|
56
|
+
* directory would corrupt the mount, so only the first claimant mounts and
|
|
57
|
+
* the collision is logged loudly. (The sequential code this replaced
|
|
58
|
+
* silently let the last writer win — no better, just quieter.)
|
|
52
59
|
*/
|
|
53
|
-
export declare function
|
|
60
|
+
export declare function mountSkills(client: StigmerClient, skills: readonly Skill[], platformDir: string): Promise<SkillPathMap>;
|
|
54
61
|
/**
|
|
55
62
|
* Generate the system prompt section following the Agent Skills spec
|
|
56
63
|
* progressive disclosure model.
|
|
@@ -66,8 +73,3 @@ export declare function generatePromptSection(skills: readonly Skill[], skillPat
|
|
|
66
73
|
* they are needed mid-conversation.
|
|
67
74
|
*/
|
|
68
75
|
export declare function generateAlsoAvailableSection(excludedNames: readonly string[]): string;
|
|
69
|
-
/**
|
|
70
|
-
* Download skill artifacts for all skills that have a storage key.
|
|
71
|
-
* Returns a map from skill ID to artifact bytes.
|
|
72
|
-
*/
|
|
73
|
-
export declare function fetchSkillArtifacts(client: StigmerClient, skills: readonly Skill[]): Promise<Map<string, Uint8Array>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill
|
|
2
|
+
* Skill mounting and prompt generation for the deep-agent execution path.
|
|
3
3
|
*
|
|
4
4
|
* Follows the Agent Skills specification progressive disclosure model:
|
|
5
5
|
*
|
|
@@ -10,13 +10,23 @@
|
|
|
10
10
|
* 3. Resources (on demand) — scripts, references, and assets are
|
|
11
11
|
* loaded by the agent only when required.
|
|
12
12
|
*
|
|
13
|
-
* Skills live
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Skills physically live in the session's platform directory
|
|
14
|
+
* (`{platformDir}/skills/{name}/` — the SAME location the Cursor harness
|
|
15
|
+
* mounts into), and the agent sees them as `.stigmer/skills/{name}/`
|
|
16
|
+
* through the per-turn workspace symlink (see workspace/stigmer-link.ts).
|
|
17
|
+
* Returned paths are the agent-visible `.stigmer/…` form.
|
|
18
|
+
*
|
|
19
|
+
* Mounts are cached by the skill's content-addressed version hash via the
|
|
20
|
+
* shared skill-mount mechanics (issue #337, mirroring the Cursor harness's
|
|
21
|
+
* #672 fix): metadata is fetched every execution — a pushed skill update
|
|
22
|
+
* still lands on the very next message — but an unchanged skill skips the
|
|
23
|
+
* artifact download and rewrite entirely.
|
|
16
24
|
*/
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { SKILLS_SUBDIR, mountIsFresh, downloadArtifact, writeSkillMount, } from "./skill-mount.js";
|
|
27
|
+
import { STIGMER_LOCAL_STATE_DIR } from "./workspace/stigmer-link.js";
|
|
28
|
+
/** Agent-visible base of the skills tree (via the workspace `.stigmer` symlink). */
|
|
29
|
+
const SKILLS_RELATIVE_BASE = `${STIGMER_LOCAL_STATE_DIR}/${SKILLS_SUBDIR}`;
|
|
20
30
|
// ─── Fetch ───────────────────────────────────────────────────────────────
|
|
21
31
|
/**
|
|
22
32
|
* Merge skill refs from agent and session specs (union, dedup by slug,
|
|
@@ -59,87 +69,80 @@ export async function fetchSkillsByRefs(client, refs) {
|
|
|
59
69
|
}
|
|
60
70
|
return results;
|
|
61
71
|
}
|
|
62
|
-
// ───
|
|
72
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
63
73
|
/**
|
|
64
|
-
*
|
|
65
|
-
* skill
|
|
74
|
+
* Mount skills into the session's platform directory, downloading each
|
|
75
|
+
* skill's artifact only on a version-hash cache miss (see skill-mount.ts
|
|
76
|
+
* for the marker mechanics). Returns a map of skill-id -> agent-visible
|
|
77
|
+
* directory path.
|
|
78
|
+
*
|
|
79
|
+
* The returned map is a NAMING function, not a success record: every skill
|
|
80
|
+
* gets its path entry so prompt generation stays total, while mount
|
|
81
|
+
* failures degrade that one skill (loud log, no throw) — one broken skill
|
|
82
|
+
* must never kill the run.
|
|
66
83
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
84
|
+
* Skills mount in parallel (each owns an independent directory), EXCEPT
|
|
85
|
+
* when two skills resolve to the same directory name (same `spec.name`
|
|
86
|
+
* from different orgs): concurrent remove-and-rewrite passes on one
|
|
87
|
+
* directory would corrupt the mount, so only the first claimant mounts and
|
|
88
|
+
* the collision is logged loudly. (The sequential code this replaced
|
|
89
|
+
* silently let the last writer win — no better, just quieter.)
|
|
71
90
|
*/
|
|
72
|
-
export async function
|
|
91
|
+
export async function mountSkills(client, skills, platformDir) {
|
|
73
92
|
const paths = new Map();
|
|
93
|
+
const claims = new Map();
|
|
74
94
|
for (const skill of skills) {
|
|
75
95
|
const name = skill.spec?.name || skill.metadata?.slug || "unknown";
|
|
76
96
|
const skillId = skill.metadata?.id ?? name;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await extractZipToWorkspaceExcluding(artifactBytes, "SKILL.md", relativeDir, workspaceBackend);
|
|
85
|
-
}
|
|
97
|
+
paths.set(skillId, `${SKILLS_RELATIVE_BASE}/${name}`);
|
|
98
|
+
// Impossible through the push pipeline (push.go hard-fails a ZIP
|
|
99
|
+
// without an extractable SKILL.md and always populates spec.skill_md),
|
|
100
|
+
// so an empty skillMd means a broken resource — skip, same as Cursor.
|
|
101
|
+
if (!skill.spec?.skillMd) {
|
|
102
|
+
console.warn(`[skill-writer] skill ${name} (${skillId}) has no skillMd content — skipping mount`);
|
|
103
|
+
continue;
|
|
86
104
|
}
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
const prev = claims.get(name);
|
|
106
|
+
if (prev) {
|
|
107
|
+
console.warn(`[skill-writer] mount directory collision on '${name}': ` +
|
|
108
|
+
`${prev.metadata?.org}/${prev.metadata?.slug} already claims it, ` +
|
|
109
|
+
`skipping ${skill.metadata?.org}/${skill.metadata?.slug}`);
|
|
110
|
+
continue;
|
|
89
111
|
}
|
|
90
|
-
|
|
112
|
+
claims.set(name, skill);
|
|
91
113
|
}
|
|
114
|
+
await Promise.all([...claims.entries()].map(async ([name, skill]) => {
|
|
115
|
+
const skillDir = join(platformDir, SKILLS_SUBDIR, name);
|
|
116
|
+
try {
|
|
117
|
+
const versionHash = skill.status?.versionHash ?? "";
|
|
118
|
+
const wantsArtifact = Boolean(skill.status?.artifactStorageKey);
|
|
119
|
+
if (versionHash !== "" && (await mountIsFresh(skillDir, versionHash, wantsArtifact))) {
|
|
120
|
+
console.log(`[skill-writer] mount cache hit: ${name} (version ${versionHash.slice(0, 12)}) — skipping artifact transfer`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let artifactBytes;
|
|
124
|
+
if (wantsArtifact) {
|
|
125
|
+
try {
|
|
126
|
+
artifactBytes = await downloadArtifact(client, skill.status.artifactStorageKey);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Deliberate degradation, but LOUD (#675): the run still gets
|
|
130
|
+
// SKILL.md, and the marker records artifactMounted=false so the
|
|
131
|
+
// next execution retries the download.
|
|
132
|
+
console.error(`[skill-writer] artifact download FAILED for ${name} ` +
|
|
133
|
+
`(key=${skill.status.artifactStorageKey}) — mounting SKILL.md WITHOUT the skill's ` +
|
|
134
|
+
`supporting files (scripts/references will be missing): ${err instanceof Error ? err.message : err}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await writeSkillMount(skill, skillDir, artifactBytes);
|
|
138
|
+
console.log(`[skill-writer] wrote skill mount: ${name}`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.warn(`[skill-writer] failed to mount skill ${name}: ${err instanceof Error ? err.message : err}`);
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
92
144
|
return { paths };
|
|
93
145
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Compute skill paths without writing anything (for resume integrity checks).
|
|
96
|
-
*/
|
|
97
|
-
export function computeSkillPaths(skills) {
|
|
98
|
-
const paths = new Map();
|
|
99
|
-
for (const skill of skills) {
|
|
100
|
-
const name = skill.spec?.name || skill.metadata?.slug || "unknown";
|
|
101
|
-
const skillId = skill.metadata?.id ?? name;
|
|
102
|
-
paths.set(skillId, `${SKILLS_RELATIVE_BASE}/${name}`);
|
|
103
|
-
}
|
|
104
|
-
return paths;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Check workspace integrity for resume fast-path.
|
|
108
|
-
* Returns true if the sentinel SKILL.md file exists for the first skill.
|
|
109
|
-
*/
|
|
110
|
-
export async function checkSkillIntegrity(skills, workspaceBackend) {
|
|
111
|
-
if (skills.length === 0)
|
|
112
|
-
return true;
|
|
113
|
-
const paths = computeSkillPaths(skills);
|
|
114
|
-
const firstPath = paths.values().next().value;
|
|
115
|
-
if (!firstPath)
|
|
116
|
-
return true;
|
|
117
|
-
const sentinel = `${firstPath}/SKILL.md`;
|
|
118
|
-
return workspaceBackend.exists(sentinel);
|
|
119
|
-
}
|
|
120
|
-
// ─── ZIP extraction ──────────────────────────────────────────────────────
|
|
121
|
-
async function extractZipToWorkspace(zipBytes, targetDir, backend) {
|
|
122
|
-
const entries = await extractZipFileEntries(zipBytes);
|
|
123
|
-
for (const entry of entries) {
|
|
124
|
-
await backend.writeFileBuffer(`${targetDir}/${entry.path}`, Buffer.from(entry.content));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
async function extractZipToWorkspaceExcluding(zipBytes, excludeName, targetDir, backend) {
|
|
128
|
-
const entries = await extractZipFileEntries(zipBytes, { exclude: [excludeName] });
|
|
129
|
-
for (const entry of entries) {
|
|
130
|
-
await backend.writeFileBuffer(`${targetDir}/${entry.path}`, Buffer.from(entry.content));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async function makeScriptsExecutable(relativeDir, backend) {
|
|
134
|
-
const extensions = [...SCRIPT_EXTENSIONS].map(ext => `-name '*${ext}'`).join(" -o ");
|
|
135
|
-
const cmd = `find ${relativeDir} -type f \\( ${extensions} \\) -exec chmod +x {} \\; 2>/dev/null || true`;
|
|
136
|
-
try {
|
|
137
|
-
await backend.execute(cmd);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Non-fatal: scripts may not be executable in all environments
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
146
|
// ─── Prompt generation ───────────────────────────────────────────────────
|
|
144
147
|
/**
|
|
145
148
|
* Generate the system prompt section following the Agent Skills spec
|
|
@@ -201,30 +204,4 @@ export function generateAlsoAvailableSection(excludedNames) {
|
|
|
201
204
|
"If you determine one of them is relevant to your task, " +
|
|
202
205
|
"read its SKILL.md at `.stigmer/skills/<name>/SKILL.md` to activate it.\n");
|
|
203
206
|
}
|
|
204
|
-
// ─── Artifact fetching ───────────────────────────────────────────────────
|
|
205
|
-
/**
|
|
206
|
-
* Download skill artifacts for all skills that have a storage key.
|
|
207
|
-
* Returns a map from skill ID to artifact bytes.
|
|
208
|
-
*/
|
|
209
|
-
export async function fetchSkillArtifacts(client, skills) {
|
|
210
|
-
const artifacts = new Map();
|
|
211
|
-
const fetches = skills
|
|
212
|
-
.filter(s => s.status?.artifactStorageKey)
|
|
213
|
-
.map(async (skill) => {
|
|
214
|
-
const key = skill.status.artifactStorageKey;
|
|
215
|
-
const skillId = skill.metadata?.id ?? skill.spec?.name ?? "unknown";
|
|
216
|
-
try {
|
|
217
|
-
const response = await client.getSkillArtifact(key);
|
|
218
|
-
if (response.artifact && response.artifact.length > 0) {
|
|
219
|
-
artifacts.set(skillId, response.artifact);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
console.warn(`[skill-writer] Failed to download artifact for ${skill.spec?.name}: ` +
|
|
224
|
-
`${err instanceof Error ? err.message : String(err)}. Falling back to SKILL.md only.`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
await Promise.all(fetches);
|
|
228
|
-
return artifacts;
|
|
229
|
-
}
|
|
230
207
|
//# sourceMappingURL=skill-writer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-writer.js","sourceRoot":"","sources":["../../src/shared/skill-writer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"skill-writer.js","sourceRoot":"","sources":["../../src/shared/skill-writer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task-queue worker-shutdown signals — the classification channel that
|
|
3
|
+
* lets an in-flight activity distinguish "my worker is shutting down" from
|
|
4
|
+
* "the orchestrator cancelled me" (a user pause).
|
|
5
|
+
*
|
|
6
|
+
* The signal carries NO lifecycle authority: aborting it never stops a
|
|
7
|
+
* worker or an activity (Temporal's own drain does that). It exists purely
|
|
8
|
+
* so the activity's cancellation handling can classify the interruption
|
|
9
|
+
* honestly — a shutdown is not a pause, and must surface as the
|
|
10
|
+
* worker-shutdown failure shape the control planes recognize (issue #776).
|
|
11
|
+
*
|
|
12
|
+
* Ownership contract:
|
|
13
|
+
* - whoever creates a Worker registers a signal for its queue BEFORE the
|
|
14
|
+
* worker starts polling;
|
|
15
|
+
* - whoever initiates a full shutdown (SIGTERM handler, desktop quit)
|
|
16
|
+
* aborts the signal BEFORE calling worker.shutdown(), so activities
|
|
17
|
+
* cancelled by the drain observe it already aborted;
|
|
18
|
+
* - a graceful single-worker teardown (view close, deferred teardown in
|
|
19
|
+
* the runner-manager) must NOT abort — those paths only run once no
|
|
20
|
+
* activity is in flight, and aborting earlier is exactly the regression
|
|
21
|
+
* that killed running activities on a view close.
|
|
22
|
+
*
|
|
23
|
+
* Module-level (not per-manager) because activities resolve their signal by
|
|
24
|
+
* task-queue name via {@link getShutdownSignalForQueue} without a handle to
|
|
25
|
+
* the runner/manager instance that created their worker.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Register a fresh shutdown signal for a task queue, replacing any previous
|
|
29
|
+
* registration (a re-created worker on a reused queue must not observe the
|
|
30
|
+
* old worker's aborted signal). Returns the controller so the worker's owner
|
|
31
|
+
* can abort it at shutdown.
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerWorkerShutdownSignal(taskQueue: string): AbortController;
|
|
34
|
+
/** The signal an activity on `taskQueue` should observe, if one is registered. */
|
|
35
|
+
export declare function getShutdownSignalForQueue(taskQueue: string): AbortSignal | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Abort the queue's signal, marking any in-flight activity's imminent
|
|
38
|
+
* cancellation as a worker shutdown. Idempotent; no-op for an unknown queue.
|
|
39
|
+
*/
|
|
40
|
+
export declare function signalWorkerShutdown(taskQueue: string): void;
|
|
41
|
+
/** Drop the registration once the queue's worker is fully torn down. */
|
|
42
|
+
export declare function unregisterWorkerShutdownSignal(taskQueue: string): void;
|
|
43
|
+
/** The evidence a turn's post-stream classification weighs (see below). */
|
|
44
|
+
export interface TurnInterruptionEvidence {
|
|
45
|
+
/** The periodic heartbeat threw CancelledFailure with no shutdown signal. */
|
|
46
|
+
heartbeatCancelled: boolean;
|
|
47
|
+
/** The periodic heartbeat threw CancelledFailure with the signal aborted. */
|
|
48
|
+
heartbeatWorkerShutdown: boolean;
|
|
49
|
+
/** Temporal delivered cancellation to the activity. */
|
|
50
|
+
cancellationSignalAborted: boolean;
|
|
51
|
+
/** This queue's worker-shutdown signal is aborted. */
|
|
52
|
+
shutdownSignalAborted: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Classify how (whether) a turn's stream was interrupted, from the evidence
|
|
56
|
+
* available after the stream ends. Pure so the decision table is directly
|
|
57
|
+
* testable — the activities feed it their heartbeat flags and signals.
|
|
58
|
+
*
|
|
59
|
+
* The load-bearing rule is the grace-window guard (#776): an aborted
|
|
60
|
+
* shutdown signal ALONE is "none", because a run that completes normally
|
|
61
|
+
* inside the drain grace window reaches the classification with the signal
|
|
62
|
+
* already aborted and nothing actually interrupted. Shutdown requires
|
|
63
|
+
* interruption evidence (a heartbeat CancelledFailure or a delivered
|
|
64
|
+
* cancellation) alongside the signal; a heartbeat cancellation with no
|
|
65
|
+
* shutdown signal is the orchestrator's pause.
|
|
66
|
+
*/
|
|
67
|
+
export declare function classifyTurnInterruption(evidence: TurnInterruptionEvidence): "worker-shutdown" | "pause" | "none";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-task-queue worker-shutdown signals — the classification channel that
|
|
3
|
+
* lets an in-flight activity distinguish "my worker is shutting down" from
|
|
4
|
+
* "the orchestrator cancelled me" (a user pause).
|
|
5
|
+
*
|
|
6
|
+
* The signal carries NO lifecycle authority: aborting it never stops a
|
|
7
|
+
* worker or an activity (Temporal's own drain does that). It exists purely
|
|
8
|
+
* so the activity's cancellation handling can classify the interruption
|
|
9
|
+
* honestly — a shutdown is not a pause, and must surface as the
|
|
10
|
+
* worker-shutdown failure shape the control planes recognize (issue #776).
|
|
11
|
+
*
|
|
12
|
+
* Ownership contract:
|
|
13
|
+
* - whoever creates a Worker registers a signal for its queue BEFORE the
|
|
14
|
+
* worker starts polling;
|
|
15
|
+
* - whoever initiates a full shutdown (SIGTERM handler, desktop quit)
|
|
16
|
+
* aborts the signal BEFORE calling worker.shutdown(), so activities
|
|
17
|
+
* cancelled by the drain observe it already aborted;
|
|
18
|
+
* - a graceful single-worker teardown (view close, deferred teardown in
|
|
19
|
+
* the runner-manager) must NOT abort — those paths only run once no
|
|
20
|
+
* activity is in flight, and aborting earlier is exactly the regression
|
|
21
|
+
* that killed running activities on a view close.
|
|
22
|
+
*
|
|
23
|
+
* Module-level (not per-manager) because activities resolve their signal by
|
|
24
|
+
* task-queue name via {@link getShutdownSignalForQueue} without a handle to
|
|
25
|
+
* the runner/manager instance that created their worker.
|
|
26
|
+
*/
|
|
27
|
+
const registry = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Register a fresh shutdown signal for a task queue, replacing any previous
|
|
30
|
+
* registration (a re-created worker on a reused queue must not observe the
|
|
31
|
+
* old worker's aborted signal). Returns the controller so the worker's owner
|
|
32
|
+
* can abort it at shutdown.
|
|
33
|
+
*/
|
|
34
|
+
export function registerWorkerShutdownSignal(taskQueue) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
registry.set(taskQueue, controller);
|
|
37
|
+
return controller;
|
|
38
|
+
}
|
|
39
|
+
/** The signal an activity on `taskQueue` should observe, if one is registered. */
|
|
40
|
+
export function getShutdownSignalForQueue(taskQueue) {
|
|
41
|
+
return registry.get(taskQueue)?.signal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Abort the queue's signal, marking any in-flight activity's imminent
|
|
45
|
+
* cancellation as a worker shutdown. Idempotent; no-op for an unknown queue.
|
|
46
|
+
*/
|
|
47
|
+
export function signalWorkerShutdown(taskQueue) {
|
|
48
|
+
registry.get(taskQueue)?.abort();
|
|
49
|
+
}
|
|
50
|
+
/** Drop the registration once the queue's worker is fully torn down. */
|
|
51
|
+
export function unregisterWorkerShutdownSignal(taskQueue) {
|
|
52
|
+
registry.delete(taskQueue);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Classify how (whether) a turn's stream was interrupted, from the evidence
|
|
56
|
+
* available after the stream ends. Pure so the decision table is directly
|
|
57
|
+
* testable — the activities feed it their heartbeat flags and signals.
|
|
58
|
+
*
|
|
59
|
+
* The load-bearing rule is the grace-window guard (#776): an aborted
|
|
60
|
+
* shutdown signal ALONE is "none", because a run that completes normally
|
|
61
|
+
* inside the drain grace window reaches the classification with the signal
|
|
62
|
+
* already aborted and nothing actually interrupted. Shutdown requires
|
|
63
|
+
* interruption evidence (a heartbeat CancelledFailure or a delivered
|
|
64
|
+
* cancellation) alongside the signal; a heartbeat cancellation with no
|
|
65
|
+
* shutdown signal is the orchestrator's pause.
|
|
66
|
+
*/
|
|
67
|
+
export function classifyTurnInterruption(evidence) {
|
|
68
|
+
const interrupted = evidence.heartbeatCancelled ||
|
|
69
|
+
evidence.heartbeatWorkerShutdown ||
|
|
70
|
+
evidence.cancellationSignalAborted;
|
|
71
|
+
if (!interrupted) {
|
|
72
|
+
return "none";
|
|
73
|
+
}
|
|
74
|
+
if (evidence.heartbeatWorkerShutdown || evidence.shutdownSignalAborted) {
|
|
75
|
+
return "worker-shutdown";
|
|
76
|
+
}
|
|
77
|
+
return evidence.heartbeatCancelled ? "pause" : "none";
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=worker-shutdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-shutdown.js","sourceRoot":"","sources":["../../src/shared/worker-shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAAiB;IAC5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACpC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,OAAO,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,8BAA8B,CAAC,SAAiB;IAC9D,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC7B,CAAC;AAcD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAkC;IAElC,MAAM,WAAW,GACf,QAAQ,CAAC,kBAAkB;QAC3B,QAAQ,CAAC,uBAAuB;QAChC,QAAQ,CAAC,yBAAyB,CAAC;IACrC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,QAAQ,CAAC,uBAAuB,IAAI,QAAQ,CAAC,qBAAqB,EAAE,CAAC;QACvE,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,OAAO,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AACxD,CAAC"}
|