@xemahq/opencode-xema-plugin 0.3.0 → 0.4.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/README.md +33 -40
- package/dist/.source-stamp.json +2 -2
- package/dist/context.d.ts +5 -14
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -13
- package/dist/context.js.map +1 -1
- package/dist/hooks/compaction-invariants.d.ts +8 -0
- package/dist/hooks/compaction-invariants.d.ts.map +1 -0
- package/dist/hooks/compaction-invariants.js +60 -0
- package/dist/hooks/compaction-invariants.js.map +1 -0
- package/dist/hooks/root-turn-completion.d.ts +14 -0
- package/dist/hooks/root-turn-completion.d.ts.map +1 -0
- package/dist/hooks/root-turn-completion.js +84 -0
- package/dist/hooks/root-turn-completion.js.map +1 -0
- package/dist/hooks/shell-env-hygiene.d.ts +6 -0
- package/dist/hooks/shell-env-hygiene.d.ts.map +1 -0
- package/dist/hooks/shell-env-hygiene.js +10 -0
- package/dist/hooks/shell-env-hygiene.js.map +1 -0
- package/dist/hooks/subagent-fanout-ceiling.d.ts +18 -17
- package/dist/hooks/subagent-fanout-ceiling.d.ts.map +1 -1
- package/dist/hooks/subagent-fanout-ceiling.js +106 -92
- package/dist/hooks/subagent-fanout-ceiling.js.map +1 -1
- package/dist/hooks/system-overlay.d.ts +6 -0
- package/dist/hooks/system-overlay.d.ts.map +1 -0
- package/dist/hooks/system-overlay.js +40 -0
- package/dist/hooks/system-overlay.js.map +1 -0
- package/dist/hooks/tool-surface-observation.d.ts +14 -0
- package/dist/hooks/tool-surface-observation.d.ts.map +1 -0
- package/dist/hooks/tool-surface-observation.js +30 -0
- package/dist/hooks/tool-surface-observation.js.map +1 -0
- package/dist/hooks/workspace-listing.js +3 -3
- package/dist/hooks/workspace-listing.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -70
- package/dist/index.js.map +1 -1
- package/dist/plugin-id.d.ts +2 -0
- package/dist/plugin-id.d.ts.map +1 -0
- package/dist/plugin-id.js +2 -0
- package/dist/plugin-id.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +86 -0
- package/dist/plugin.js.map +1 -0
- package/dist/runtime-events.d.ts +17 -0
- package/dist/runtime-events.d.ts.map +1 -0
- package/dist/runtime-events.js +39 -0
- package/dist/runtime-events.js.map +1 -0
- package/package.json +12 -16
- package/{plugin.mjs → plugin/xema-runtime-bridge/index.js} +407 -415
- package/plugin/xema-runtime-bridge/package.json +8 -0
- package/dist/hooks/session-idle-autocommit.d.ts +0 -11
- package/dist/hooks/session-idle-autocommit.d.ts.map +0 -1
- package/dist/hooks/session-idle-autocommit.js +0 -206
- package/dist/hooks/session-idle-autocommit.js.map +0 -1
- package/dist/hooks/system-prompt-overlay.d.ts +0 -3
- package/dist/hooks/system-prompt-overlay.d.ts.map +0 -1
- package/dist/hooks/system-prompt-overlay.js +0 -30
- package/dist/hooks/system-prompt-overlay.js.map +0 -1
- package/dist/opencode-types.d.ts +0 -49
- package/dist/opencode-types.d.ts.map +0 -1
- package/dist/opencode-types.js +0 -2
- package/dist/opencode-types.js.map +0 -1
- package/dist/plugin-entry.d.ts +0 -2
- package/dist/plugin-entry.d.ts.map +0 -1
- package/dist/plugin-entry.js +0 -2
- package/dist/plugin-entry.js.map +0 -1
|
@@ -4,16 +4,6 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/index.ts
|
|
8
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
9
|
-
import { join as join5 } from "node:path";
|
|
10
|
-
|
|
11
|
-
// src/hooks/session-idle-autocommit.ts
|
|
12
|
-
import { execFile } from "node:child_process";
|
|
13
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
14
|
-
import { join as join2 } from "node:path";
|
|
15
|
-
import { promisify } from "node:util";
|
|
16
|
-
|
|
17
7
|
// src/context.ts
|
|
18
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
9
|
import { join } from "node:path";
|
|
@@ -19703,9 +19693,6 @@ function debug(msg) {
|
|
|
19703
19693
|
if (!DEBUG_ENABLED) return;
|
|
19704
19694
|
console.log(`${PREFIX} [debug] ${msg}`);
|
|
19705
19695
|
}
|
|
19706
|
-
function isDebug() {
|
|
19707
|
-
return DEBUG_ENABLED;
|
|
19708
|
-
}
|
|
19709
19696
|
|
|
19710
19697
|
// src/context.ts
|
|
19711
19698
|
var ContextJsonSchema = external_exports.object({
|
|
@@ -19730,7 +19717,8 @@ var ContextJsonSchema = external_exports.object({
|
|
|
19730
19717
|
// reviewable change set from those edits.
|
|
19731
19718
|
"document"
|
|
19732
19719
|
]),
|
|
19733
|
-
surface: external_exports.enum(["pipeline-run", "agent-session", "design-system-builder", "gate-review"]).optional()
|
|
19720
|
+
surface: external_exports.enum(["pipeline-run", "agent-session", "design-system-builder", "gate-review"]).optional(),
|
|
19721
|
+
stageKey: external_exports.string().nullish()
|
|
19734
19722
|
}),
|
|
19735
19723
|
project: external_exports.object({
|
|
19736
19724
|
orgId: external_exports.string(),
|
|
@@ -19767,35 +19755,11 @@ var ContextJsonSchema = external_exports.object({
|
|
|
19767
19755
|
// ship the llm-registry-api that drops the field.
|
|
19768
19756
|
}),
|
|
19769
19757
|
/**
|
|
19770
|
-
*
|
|
19771
|
-
*
|
|
19772
|
-
*
|
|
19773
|
-
* (`shared_dev_publish_to_prod` — the default). Absent / `disabled` for
|
|
19774
|
-
* `branch_per_session` sessions, where commits are user-controlled.
|
|
19775
|
-
*
|
|
19776
|
-
* `repoSubPath` is the working repo's path relative to the workspace
|
|
19777
|
-
* root. When omitted, the hook commits at the workspace root itself
|
|
19778
|
-
* (single-repo workspace contract — multi-repo workspaces are handled
|
|
19779
|
-
* by a future follow-up; see TODO(multi-repo) in the
|
|
19780
|
-
* hook).
|
|
19758
|
+
* Working files the platform materialised (kernel `ContextJson.workingFiles`).
|
|
19759
|
+
* Only `slug` and `path` are read — by the compaction hook, which carries
|
|
19760
|
+
* them as continuation invariants. Unread fields are not validated here.
|
|
19781
19761
|
*/
|
|
19782
|
-
|
|
19783
|
-
autoCommit: external_exports.enum(["enabled", "disabled"]),
|
|
19784
|
-
repoSubPath: external_exports.string().nullish()
|
|
19785
|
-
}).nullish(),
|
|
19786
|
-
/**
|
|
19787
|
-
* Optional `actor` block consumed by the `session.idle` auto-commit
|
|
19788
|
-
* hook. Mirrors `ActorRefDto` in workspace-git-api: the subject that
|
|
19789
|
-
* triggered the turn. Stamped at session-start by the orchestrator
|
|
19790
|
-
* (single-user binding) or rotated per-turn by the multi-user session
|
|
19791
|
-
* extension. Absent for non-session invocations (pipeline-run agents
|
|
19792
|
-
* never auto-commit).
|
|
19793
|
-
*/
|
|
19794
|
-
actor: external_exports.object({
|
|
19795
|
-
kind: external_exports.enum(["org_user"]),
|
|
19796
|
-
subjectRef: external_exports.string().min(1),
|
|
19797
|
-
displayName: external_exports.string().min(1)
|
|
19798
|
-
}).nullish()
|
|
19762
|
+
workingFiles: external_exports.array(external_exports.object({ slug: external_exports.string(), path: external_exports.string() }).loose()).optional()
|
|
19799
19763
|
});
|
|
19800
19764
|
function readInvocationContextForHook(workspaceDir, hookName) {
|
|
19801
19765
|
const path = join(workspaceDir, "context.json");
|
|
@@ -19815,220 +19779,186 @@ function readInvocationContextForHook(workspaceDir, hookName) {
|
|
|
19815
19779
|
}
|
|
19816
19780
|
}
|
|
19817
19781
|
|
|
19818
|
-
// src/hooks/
|
|
19819
|
-
var
|
|
19820
|
-
var
|
|
19821
|
-
var
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
return async (input2) => {
|
|
19827
|
-
const evt = input2.event;
|
|
19828
|
-
if (evt.type !== "session.idle") {
|
|
19782
|
+
// src/hooks/compaction-invariants.ts
|
|
19783
|
+
var XEMA_COMPACTION_METADATA_KEY = "xema.compaction-invariants";
|
|
19784
|
+
var MAX_COMPACTION_INVARIANTS_CHARS = 2e3;
|
|
19785
|
+
var MAX_LISTED_ITEMS = 20;
|
|
19786
|
+
function buildCompactionInvariantsHook(directory) {
|
|
19787
|
+
return (input2) => {
|
|
19788
|
+
const ctx = readInvocationContextForHook(directory, "session.compaction");
|
|
19789
|
+
if (!ctx) {
|
|
19829
19790
|
return;
|
|
19830
19791
|
}
|
|
19831
|
-
const
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
)
|
|
19792
|
+
const text = renderCompactionInvariants(ctx);
|
|
19793
|
+
const part = {
|
|
19794
|
+
type: "text",
|
|
19795
|
+
text,
|
|
19796
|
+
metadata: { [XEMA_COMPACTION_METADATA_KEY]: true }
|
|
19797
|
+
};
|
|
19798
|
+
const existing = input2.system.findIndex(
|
|
19799
|
+
(entry) => entry.metadata?.[XEMA_COMPACTION_METADATA_KEY] === true
|
|
19800
|
+
);
|
|
19801
|
+
if (existing === -1) {
|
|
19802
|
+
input2.system.push(part);
|
|
19803
|
+
} else {
|
|
19804
|
+
input2.system[existing] = part;
|
|
19839
19805
|
}
|
|
19806
|
+
debug(`session.compaction: contributed ${text.length} chars of continuation invariants`);
|
|
19840
19807
|
};
|
|
19841
19808
|
}
|
|
19842
|
-
|
|
19843
|
-
const
|
|
19844
|
-
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
const
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
);
|
|
19856
|
-
return;
|
|
19857
|
-
}
|
|
19858
|
-
if (!ctx.actor) {
|
|
19859
|
-
warn(
|
|
19860
|
-
`hook=${HOOK_NAME}: session=${sessionId} has git.autoCommit=enabled but no actor in context.json; skipping. The orchestrator must stamp context.actor for sessions opted into per-turn auto-commit.`
|
|
19861
|
-
);
|
|
19862
|
-
return;
|
|
19863
|
-
}
|
|
19864
|
-
const apiUrl = process.env.WORKSPACE_GIT_API_URL;
|
|
19865
|
-
if (!apiUrl || apiUrl.length === 0) {
|
|
19866
|
-
warn(
|
|
19867
|
-
`hook=${HOOK_NAME}: session=${sessionId} WORKSPACE_GIT_API_URL env not set; skipping auto-commit. Wire the env into the worker container envelope.`
|
|
19868
|
-
);
|
|
19869
|
-
return;
|
|
19809
|
+
function listed(items) {
|
|
19810
|
+
const shown = items.slice(0, MAX_LISTED_ITEMS).join(", ");
|
|
19811
|
+
const more = items.length - MAX_LISTED_ITEMS;
|
|
19812
|
+
return more > 0 ? `${shown} (+${more} more)` : shown;
|
|
19813
|
+
}
|
|
19814
|
+
function renderCompactionInvariants(ctx) {
|
|
19815
|
+
const inv = ctx.invocation;
|
|
19816
|
+
const lines = [
|
|
19817
|
+
'Xema continuation invariants. Preserve these facts verbatim in the summary (under "## Additional Context"); they identify the invocation and its deliverable contract and must survive compaction. Do not invent anything beyond them.',
|
|
19818
|
+
`- invocation: role=${inv.role}` + (inv.surface ? `, surface=${inv.surface}` : "") + (inv.runId ? `, runId=${inv.runId}` : "") + (inv.stageKey ? `, stageKey=${inv.stageKey}` : "") + (inv.sessionId ? `, sessionId=${inv.sessionId}` : "") + `, attempt=${inv.attempt}`,
|
|
19819
|
+
`- writePolicy: ${ctx.authority.writePolicy}`
|
|
19820
|
+
];
|
|
19821
|
+
if (ctx.authority.mayWriteSlugs.length > 0) {
|
|
19822
|
+
lines.push(`- deliverable slugs: ${listed(ctx.authority.mayWriteSlugs)}`);
|
|
19870
19823
|
}
|
|
19871
|
-
const
|
|
19872
|
-
|
|
19873
|
-
|
|
19874
|
-
warn(
|
|
19875
|
-
`hook=${HOOK_NAME}: session=${sessionId} actor JWT not readable at ${jwtPath}; skipping auto-commit. Verify the orchestrator mounts the actor JWT before session start.`
|
|
19876
|
-
);
|
|
19877
|
-
return;
|
|
19824
|
+
const files = ctx.workingFiles ?? [];
|
|
19825
|
+
if (files.length > 0) {
|
|
19826
|
+
lines.push(`- working files: ${listed(files.map((file2) => `${file2.slug} \u2192 ${file2.path}`))}`);
|
|
19878
19827
|
}
|
|
19879
|
-
const
|
|
19880
|
-
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19828
|
+
const text = lines.join("\n");
|
|
19829
|
+
return text.length > MAX_COMPACTION_INVARIANTS_CHARS ? `${text.slice(0, MAX_COMPACTION_INVARIANTS_CHARS - 1)}\u2026` : text;
|
|
19830
|
+
}
|
|
19831
|
+
|
|
19832
|
+
// src/runtime-events.ts
|
|
19833
|
+
var TERMINAL_EVENT_TYPES = Object.freeze({
|
|
19834
|
+
"session.execution.succeeded": "succeeded" /* Succeeded */,
|
|
19835
|
+
"session.execution.failed": "failed" /* Failed */,
|
|
19836
|
+
"session.execution.interrupted": "interrupted" /* Interrupted */
|
|
19837
|
+
});
|
|
19838
|
+
function isRecord(value) {
|
|
19839
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19840
|
+
}
|
|
19841
|
+
function parseRuntimeSessionEvent(event) {
|
|
19842
|
+
if (!isRecord(event) || typeof event.type !== "string" || !isRecord(event.data)) {
|
|
19843
|
+
return void 0;
|
|
19884
19844
|
}
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
);
|
|
19889
|
-
return;
|
|
19845
|
+
const sessionId = event.data.sessionID;
|
|
19846
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
19847
|
+
return void 0;
|
|
19890
19848
|
}
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
|
|
19895
|
-
|
|
19896
|
-
|
|
19897
|
-
|
|
19898
|
-
},
|
|
19899
|
-
// Wire contract: the worker reports the OUTCOME. For the v1
|
|
19900
|
-
// hook we forward "needs commit; service will perform / record it
|
|
19901
|
-
// via the workspace-proxy bridge once available". Until then
|
|
19902
|
-
// workspace-git-api treats `committed=true` as "you produced a
|
|
19903
|
-
// commit locally, please record it"; the actual git push happens
|
|
19904
|
-
// through the bridge.
|
|
19905
|
-
committed: true,
|
|
19906
|
-
sha: null,
|
|
19907
|
-
pushFailed: false
|
|
19908
|
-
};
|
|
19909
|
-
const endpoint = buildAutocommitUrl(apiUrl, sessionId);
|
|
19910
|
-
const response = await postAutocommit(endpoint, actorJwt, body, ctx);
|
|
19911
|
-
if (response === null) {
|
|
19912
|
-
return;
|
|
19849
|
+
if (event.type === "session.created") {
|
|
19850
|
+
const parent = event.data.parentID;
|
|
19851
|
+
return {
|
|
19852
|
+
kind: "created",
|
|
19853
|
+
sessionId,
|
|
19854
|
+
parentId: typeof parent === "string" && parent.length > 0 ? parent : void 0
|
|
19855
|
+
};
|
|
19913
19856
|
}
|
|
19914
|
-
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
`hook=${HOOK_NAME}: session=${sessionId} turn=${turn} conflict detected (remote diverged); next user message will surface the Publish affordance`
|
|
19918
|
-
);
|
|
19919
|
-
return;
|
|
19857
|
+
const outcome = Object.hasOwn(TERMINAL_EVENT_TYPES, event.type) ? TERMINAL_EVENT_TYPES[event.type] : void 0;
|
|
19858
|
+
if (outcome === void 0 || typeof event.id !== "string" || event.id.length === 0) {
|
|
19859
|
+
return void 0;
|
|
19920
19860
|
}
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
|
|
19861
|
+
return { kind: "terminal", sessionId, outcome, executionId: event.id };
|
|
19862
|
+
}
|
|
19863
|
+
|
|
19864
|
+
// src/hooks/root-turn-completion.ts
|
|
19865
|
+
var HOOK_NAME = "turn-completed";
|
|
19866
|
+
var TURN_COMPLETED_PATH = "/runtime/turn-completed";
|
|
19867
|
+
var WORKSPACE_PROXY_URL_ENV = "WORKSPACE_PROXY_URL";
|
|
19868
|
+
var SERVER_PASSWORD_ENV = "OPENCODE_SERVER_PASSWORD";
|
|
19869
|
+
var MAX_REMEMBERED_KEYS = 1e3;
|
|
19870
|
+
function createRootTurnCompletion(options) {
|
|
19871
|
+
const parentBySession = /* @__PURE__ */ new Map();
|
|
19872
|
+
const delivered = /* @__PURE__ */ new Set();
|
|
19873
|
+
async function isRoot(sessionId) {
|
|
19874
|
+
if (parentBySession.has(sessionId)) return parentBySession.get(sessionId) === void 0;
|
|
19875
|
+
try {
|
|
19876
|
+
const parent = await options.resolveParentId(sessionId);
|
|
19877
|
+
parentBySession.set(sessionId, parent);
|
|
19878
|
+
return parent === void 0;
|
|
19879
|
+
} catch (err) {
|
|
19928
19880
|
warn(
|
|
19929
|
-
`hook=${HOOK_NAME}: session=${sessionId}
|
|
19881
|
+
`hook=${HOOK_NAME}: cannot tell whether session=${sessionId} is a root (${err instanceof Error ? err.message : String(err)}); skipping rather than guessing.`
|
|
19930
19882
|
);
|
|
19931
|
-
return;
|
|
19932
|
-
}
|
|
19933
|
-
if (retry.conflictDetected) {
|
|
19934
|
-
conflictFlagBySessionId.set(sessionId, { at: Date.now(), turn });
|
|
19935
|
-
info(
|
|
19936
|
-
`hook=${HOOK_NAME}: session=${sessionId} turn=${turn} retry surfaced conflict`
|
|
19937
|
-
);
|
|
19938
|
-
return;
|
|
19883
|
+
return void 0;
|
|
19939
19884
|
}
|
|
19940
|
-
info(
|
|
19941
|
-
`hook=${HOOK_NAME}: session=${sessionId} turn=${turn} retry committed=${retry.committed} sha=${retry.sha ?? "-"}`
|
|
19942
|
-
);
|
|
19943
|
-
return;
|
|
19944
19885
|
}
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
const prev = turnCounterBySessionId.get(sessionId) ?? 0;
|
|
19951
|
-
const next = prev + 1;
|
|
19952
|
-
turnCounterBySessionId.set(sessionId, next);
|
|
19953
|
-
return next;
|
|
19954
|
-
}
|
|
19955
|
-
function readActorJwt(path) {
|
|
19956
|
-
try {
|
|
19957
|
-
if (!existsSync2(path)) {
|
|
19958
|
-
return null;
|
|
19886
|
+
function remember(key) {
|
|
19887
|
+
delivered.add(key);
|
|
19888
|
+
if (delivered.size > MAX_REMEMBERED_KEYS) {
|
|
19889
|
+
const oldest = delivered.values().next().value;
|
|
19890
|
+
if (oldest !== void 0) delivered.delete(oldest);
|
|
19959
19891
|
}
|
|
19960
|
-
const raw = readFileSync2(path, "utf-8").trim();
|
|
19961
|
-
return raw.length > 0 ? raw : null;
|
|
19962
|
-
} catch (err) {
|
|
19963
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
19964
|
-
warn(`hook=${HOOK_NAME}: failed to read actor JWT at ${path}: ${message}`);
|
|
19965
|
-
return null;
|
|
19966
|
-
}
|
|
19967
|
-
}
|
|
19968
|
-
async function isGitTreeDirty(repoDir) {
|
|
19969
|
-
try {
|
|
19970
|
-
const { stdout } = await execFileAsync(
|
|
19971
|
-
"git",
|
|
19972
|
-
["-C", repoDir, "status", "--porcelain"],
|
|
19973
|
-
{ encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 }
|
|
19974
|
-
);
|
|
19975
|
-
return stdout.trim().length > 0;
|
|
19976
|
-
} catch (err) {
|
|
19977
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
19978
|
-
warn(
|
|
19979
|
-
`hook=${HOOK_NAME}: git status failed in ${repoDir}: ${message}. Skipping auto-commit for this turn.`
|
|
19980
|
-
);
|
|
19981
|
-
return void 0;
|
|
19982
19892
|
}
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
19893
|
+
async function onSessionTerminal(sessionId, outcome, executionId) {
|
|
19894
|
+
try {
|
|
19895
|
+
if (outcome !== "succeeded" /* Succeeded */) {
|
|
19896
|
+
debug(`hook=${HOOK_NAME}: session=${sessionId} ended ${outcome}; not a commit point`);
|
|
19897
|
+
return;
|
|
19898
|
+
}
|
|
19899
|
+
const key = `${sessionId}:${executionId}`;
|
|
19900
|
+
if (delivered.has(key)) {
|
|
19901
|
+
debug(`hook=${HOOK_NAME}: ${key} already notified`);
|
|
19902
|
+
return;
|
|
19903
|
+
}
|
|
19904
|
+
if (await isRoot(sessionId) !== true) return;
|
|
19905
|
+
const env = options.env ?? process.env;
|
|
19906
|
+
const base = env[WORKSPACE_PROXY_URL_ENV];
|
|
19907
|
+
const password = env[SERVER_PASSWORD_ENV];
|
|
19908
|
+
if (!base || !password) {
|
|
19909
|
+
warn(
|
|
19910
|
+
`hook=${HOOK_NAME}: ${!base ? WORKSPACE_PROXY_URL_ENV : SERVER_PASSWORD_ENV} is not set; cannot notify workspace-proxy that session=${sessionId} completed a turn.`
|
|
19911
|
+
);
|
|
19912
|
+
return;
|
|
19913
|
+
}
|
|
19914
|
+
const url2 = `${base.replace(/\/$/, "")}${TURN_COMPLETED_PATH}`;
|
|
19915
|
+
const res = await fetch(url2, {
|
|
19916
|
+
method: "POST",
|
|
19917
|
+
headers: {
|
|
19918
|
+
"Content-Type": "application/json",
|
|
19919
|
+
Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`,
|
|
19920
|
+
"Idempotency-Key": key
|
|
19921
|
+
},
|
|
19922
|
+
body: JSON.stringify({ runtimeSessionId: sessionId, executionId, outcome })
|
|
19923
|
+
});
|
|
19924
|
+
if (!res.ok) {
|
|
19925
|
+
const text = await res.text().catch(() => "<unreadable body>");
|
|
19926
|
+
warn(`hook=${HOOK_NAME}: POST ${url2} responded ${res.status}: ${text.slice(0, 512)}`);
|
|
19927
|
+
return;
|
|
19928
|
+
}
|
|
19929
|
+
remember(key);
|
|
19930
|
+
debug(`hook=${HOOK_NAME}: notified ${key}`);
|
|
19931
|
+
} catch (err) {
|
|
20000
19932
|
warn(
|
|
20001
|
-
`hook=${HOOK_NAME}:
|
|
19933
|
+
`hook=${HOOK_NAME}: unexpected error swallowed (best-effort): ` + (err instanceof Error ? err.message : String(err))
|
|
20002
19934
|
);
|
|
20003
|
-
return null;
|
|
20004
19935
|
}
|
|
20005
|
-
const parsed = await res.json();
|
|
20006
|
-
return parsed;
|
|
20007
|
-
} catch (err) {
|
|
20008
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
20009
|
-
warn(`hook=${HOOK_NAME}: POST ${url2} failed: ${message}`);
|
|
20010
|
-
return null;
|
|
20011
19936
|
}
|
|
19937
|
+
return {
|
|
19938
|
+
onSessionCreated: (sessionId, parentId) => {
|
|
19939
|
+
parentBySession.set(sessionId, parentId);
|
|
19940
|
+
},
|
|
19941
|
+
onSessionTerminal
|
|
19942
|
+
};
|
|
20012
19943
|
}
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
19944
|
+
|
|
19945
|
+
// src/hooks/shell-env-hygiene.ts
|
|
19946
|
+
var SERVER_PASSWORD_ENV_KEYS = Object.freeze([
|
|
19947
|
+
"OPENCODE_PASSWORD",
|
|
19948
|
+
"OPENCODE_SERVER_PASSWORD"
|
|
19949
|
+
]);
|
|
19950
|
+
function scrubShellEnv(input2) {
|
|
19951
|
+
for (const key of SERVER_PASSWORD_ENV_KEYS) {
|
|
19952
|
+
delete input2.env[key];
|
|
20019
19953
|
}
|
|
20020
19954
|
}
|
|
20021
|
-
function sleep(ms) {
|
|
20022
|
-
return new Promise((resolve) => {
|
|
20023
|
-
setTimeout(resolve, ms);
|
|
20024
|
-
});
|
|
20025
|
-
}
|
|
20026
19955
|
|
|
20027
19956
|
// src/hooks/subagent-fanout-ceiling.ts
|
|
20028
|
-
var
|
|
19957
|
+
var SUBAGENT_TOOL_ID = "subagent";
|
|
20029
19958
|
var DEFAULT_SUBAGENT_FANOUT_LIMIT = 8;
|
|
20030
19959
|
var DEFAULT_FANOUT_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
20031
|
-
var
|
|
19960
|
+
var DEFAULT_ACTIVE_CHILD_TTL_MS = 30 * 60 * 1e3;
|
|
19961
|
+
var DEFAULT_PENDING_SLOT_TTL_MS = 2 * 60 * 1e3;
|
|
20032
19962
|
var FANOUT_LIMIT_ENV = "XEMA_SUBAGENT_FANOUT_LIMIT";
|
|
20033
19963
|
function resolveSubagentFanoutLimit(env = process.env) {
|
|
20034
19964
|
const declared = env[FANOUT_LIMIT_ENV];
|
|
@@ -20038,7 +19968,7 @@ function resolveSubagentFanoutLimit(env = process.env) {
|
|
|
20038
19968
|
const parsed = Number(declared.trim());
|
|
20039
19969
|
if (!Number.isInteger(parsed)) {
|
|
20040
19970
|
throw new Error(
|
|
20041
|
-
`${FANOUT_LIMIT_ENV}="${declared}" is not an integer. Expected a whole number of concurrently
|
|
19971
|
+
`${FANOUT_LIMIT_ENV}="${declared}" is not an integer. Expected a whole number of concurrently running sub-agents between 1 and ${DEFAULT_SUBAGENT_FANOUT_LIMIT}.`
|
|
20042
19972
|
);
|
|
20043
19973
|
}
|
|
20044
19974
|
if (parsed < 1) {
|
|
@@ -20056,15 +19986,17 @@ function resolveSubagentFanoutLimit(env = process.env) {
|
|
|
20056
19986
|
function createSubagentFanoutCeiling(options) {
|
|
20057
19987
|
const limit = options.limit;
|
|
20058
19988
|
const waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_FANOUT_WAIT_TIMEOUT_MS;
|
|
20059
|
-
const
|
|
19989
|
+
const pendingSlotTtlMs = options.pendingSlotTtlMs ?? DEFAULT_PENDING_SLOT_TTL_MS;
|
|
19990
|
+
const activeChildTtlMs = options.activeChildTtlMs ?? DEFAULT_ACTIVE_CHILD_TTL_MS;
|
|
20060
19991
|
const now = options.now ?? (() => Date.now());
|
|
20061
|
-
const
|
|
19992
|
+
const pending = [];
|
|
19993
|
+
const active = /* @__PURE__ */ new Map();
|
|
20062
19994
|
const waiters = [];
|
|
19995
|
+
const armed = /* @__PURE__ */ new Set();
|
|
20063
19996
|
let waitedCount = 0;
|
|
20064
19997
|
let timedOutCount = 0;
|
|
20065
|
-
let
|
|
20066
|
-
let
|
|
20067
|
-
const armed = /* @__PURE__ */ new Set();
|
|
19998
|
+
let pendingTtlReclaimedCount = 0;
|
|
19999
|
+
let activeTtlReclaimedCount = 0;
|
|
20068
20000
|
function arm(fn, ms, keepProcessAlive) {
|
|
20069
20001
|
const timer = setTimeout(() => {
|
|
20070
20002
|
armed.delete(timer);
|
|
@@ -20079,133 +20011,152 @@ function createSubagentFanoutCeiling(options) {
|
|
|
20079
20011
|
clearTimeout(timer);
|
|
20080
20012
|
armed.delete(timer);
|
|
20081
20013
|
}
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
|
|
20094
|
-
|
|
20095
|
-
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20014
|
+
const occupied = () => pending.length + active.size;
|
|
20015
|
+
function reserve(callId, parentSessionId) {
|
|
20016
|
+
const slot = { callId, parentSessionId, since: now(), timer: void 0 };
|
|
20017
|
+
slot.timer = arm(
|
|
20018
|
+
() => {
|
|
20019
|
+
const index = pending.indexOf(slot);
|
|
20020
|
+
if (index === -1) return;
|
|
20021
|
+
pending.splice(index, 1);
|
|
20022
|
+
pendingTtlReclaimedCount += 1;
|
|
20023
|
+
warn(
|
|
20024
|
+
`fanout-ceiling: reserved slot for call=${callId} session=${parentSessionId} never became a child session within ${pendingSlotTtlMs}ms \u2014 reclaiming (its execute.after was lost; V2 does not guarantee it on abort).`
|
|
20025
|
+
);
|
|
20026
|
+
drain();
|
|
20027
|
+
},
|
|
20028
|
+
pendingSlotTtlMs,
|
|
20029
|
+
false
|
|
20030
|
+
);
|
|
20031
|
+
pending.push(slot);
|
|
20032
|
+
}
|
|
20033
|
+
function activate(childSessionId, parentSessionId) {
|
|
20034
|
+
const timer = arm(
|
|
20035
|
+
() => {
|
|
20036
|
+
if (!active.has(childSessionId)) return;
|
|
20037
|
+
activeTtlReclaimedCount += 1;
|
|
20038
|
+
warn(
|
|
20039
|
+
`fanout-ceiling: child session=${childSessionId} held a slot for ${activeChildTtlMs}ms with no terminal event \u2014 force-releasing so the pool cannot shrink permanently.`
|
|
20040
|
+
);
|
|
20041
|
+
releaseActive(childSessionId, "active-ttl");
|
|
20042
|
+
},
|
|
20043
|
+
activeChildTtlMs,
|
|
20044
|
+
false
|
|
20045
|
+
);
|
|
20046
|
+
active.set(childSessionId, { childSessionId, parentSessionId, since: now(), timer });
|
|
20047
|
+
}
|
|
20048
|
+
function releaseActive(childSessionId, reason) {
|
|
20049
|
+
const child = active.get(childSessionId);
|
|
20050
|
+
if (child === void 0) return;
|
|
20051
|
+
disarm(child.timer);
|
|
20052
|
+
active.delete(childSessionId);
|
|
20053
|
+
debug(
|
|
20054
|
+
`fanout-ceiling: released child=${childSessionId} via ${reason} (held ${now() - child.since}ms, occupied=${occupied()}/${limit})`
|
|
20055
|
+
);
|
|
20103
20056
|
drain();
|
|
20104
20057
|
}
|
|
20105
20058
|
function drain() {
|
|
20106
|
-
while (waiters.length > 0 &&
|
|
20059
|
+
while (waiters.length > 0 && occupied() < limit) {
|
|
20107
20060
|
const next = waiters.shift();
|
|
20108
20061
|
if (next === void 0) return;
|
|
20109
20062
|
disarm(next.timer);
|
|
20110
|
-
const waitedMs = now() - next.since;
|
|
20111
20063
|
waitedCount += 1;
|
|
20112
|
-
|
|
20064
|
+
reserve(next.callId, next.parentSessionId);
|
|
20113
20065
|
info(
|
|
20114
|
-
`fanout-ceiling: admitted call=${next.callId} session=${next.
|
|
20066
|
+
`fanout-ceiling: admitted call=${next.callId} session=${next.parentSessionId} after waiting ${now() - next.since}ms (limit=${limit}, occupied=${occupied()}, still waiting=${waiters.length})`
|
|
20115
20067
|
);
|
|
20116
20068
|
next.grant();
|
|
20117
20069
|
}
|
|
20118
20070
|
}
|
|
20119
|
-
async function
|
|
20120
|
-
if (input2.tool !==
|
|
20121
|
-
const {
|
|
20122
|
-
if (
|
|
20123
|
-
|
|
20124
|
-
debug(
|
|
20125
|
-
`fanout-ceiling: admitted call=${callId} immediately (in-flight=${held.size}/${limit})`
|
|
20126
|
-
);
|
|
20071
|
+
async function beforeToolCall(input2) {
|
|
20072
|
+
if (input2.tool !== SUBAGENT_TOOL_ID) return;
|
|
20073
|
+
const { id: callId, sessionID: parentSessionId } = input2;
|
|
20074
|
+
if (occupied() < limit) {
|
|
20075
|
+
reserve(callId, parentSessionId);
|
|
20127
20076
|
return;
|
|
20128
20077
|
}
|
|
20129
20078
|
const since = now();
|
|
20130
20079
|
warn(
|
|
20131
|
-
`fanout-ceiling: sub-agent fan-out at ceiling \u2014 call=${callId} session=${
|
|
20080
|
+
`fanout-ceiling: sub-agent fan-out at ceiling \u2014 call=${callId} session=${parentSessionId} is WAITING (limit=${limit}, pending=${pending.length}, active=${active.size}, already waiting=${waiters.length}). Backpressure, not a refusal.`
|
|
20132
20081
|
);
|
|
20133
20082
|
await new Promise((resolve) => {
|
|
20134
|
-
const waiter = {
|
|
20135
|
-
|
|
20136
|
-
|
|
20137
|
-
|
|
20138
|
-
|
|
20139
|
-
|
|
20140
|
-
|
|
20141
|
-
|
|
20142
|
-
|
|
20143
|
-
|
|
20144
|
-
|
|
20145
|
-
|
|
20146
|
-
|
|
20147
|
-
|
|
20148
|
-
|
|
20149
|
-
|
|
20150
|
-
resolve();
|
|
20151
|
-
}, waitTimeoutMs, true);
|
|
20083
|
+
const waiter = { callId, parentSessionId, since, grant: resolve, timer: void 0 };
|
|
20084
|
+
waiter.timer = arm(
|
|
20085
|
+
() => {
|
|
20086
|
+
const index = waiters.indexOf(waiter);
|
|
20087
|
+
if (index === -1) return;
|
|
20088
|
+
waiters.splice(index, 1);
|
|
20089
|
+
timedOutCount += 1;
|
|
20090
|
+
warn(
|
|
20091
|
+
`fanout-ceiling: call=${callId} session=${parentSessionId} waited ${waitTimeoutMs}ms and is ADMITTED OVER the ceiling (limit=${limit}, occupied=${occupied()}). Suspect a leaked slot or a stuck sub-agent.`
|
|
20092
|
+
);
|
|
20093
|
+
reserve(callId, parentSessionId);
|
|
20094
|
+
resolve();
|
|
20095
|
+
},
|
|
20096
|
+
waitTimeoutMs,
|
|
20097
|
+
true
|
|
20098
|
+
);
|
|
20152
20099
|
waiters.push(waiter);
|
|
20153
20100
|
});
|
|
20154
20101
|
}
|
|
20155
|
-
function
|
|
20156
|
-
if (input2.tool !==
|
|
20157
|
-
|
|
20102
|
+
function afterToolCall(input2) {
|
|
20103
|
+
if (input2.tool !== SUBAGENT_TOOL_ID) return;
|
|
20104
|
+
const index = pending.findIndex((slot2) => slot2.callId === input2.id);
|
|
20105
|
+
if (index === -1) return;
|
|
20106
|
+
const [slot] = pending.splice(index, 1);
|
|
20107
|
+
disarm(slot?.timer);
|
|
20108
|
+
debug(`fanout-ceiling: call=${input2.id} ended before any child existed \u2014 slot released`);
|
|
20109
|
+
drain();
|
|
20158
20110
|
}
|
|
20159
|
-
function
|
|
20160
|
-
|
|
20161
|
-
|
|
20162
|
-
|
|
20111
|
+
function onSessionCreated(sessionId, parentId) {
|
|
20112
|
+
if (parentId === void 0 || active.has(sessionId)) return;
|
|
20113
|
+
const index = pending.findIndex((slot) => slot.parentSessionId === parentId);
|
|
20114
|
+
if (index === -1) {
|
|
20115
|
+
debug(`fanout-ceiling: unreserved child=${sessionId} of parent=${parentId} \u2014 counted`);
|
|
20116
|
+
} else {
|
|
20117
|
+
const [slot] = pending.splice(index, 1);
|
|
20118
|
+
disarm(slot?.timer);
|
|
20163
20119
|
}
|
|
20164
|
-
|
|
20165
|
-
|
|
20166
|
-
|
|
20167
|
-
|
|
20168
|
-
);
|
|
20169
|
-
for (const callId of stale) release(callId, "session-idle");
|
|
20120
|
+
activate(sessionId, parentId);
|
|
20121
|
+
}
|
|
20122
|
+
function onSessionTerminal(sessionId) {
|
|
20123
|
+
releaseActive(sessionId, "terminal");
|
|
20170
20124
|
}
|
|
20171
20125
|
function dispose() {
|
|
20172
|
-
const parked = waiters.length;
|
|
20173
|
-
const outstanding = held.size;
|
|
20174
20126
|
while (waiters.length > 0) {
|
|
20175
|
-
const
|
|
20176
|
-
if (
|
|
20177
|
-
disarm(
|
|
20178
|
-
|
|
20179
|
-
}
|
|
20180
|
-
for (const permit of held.values()) disarm(permit.ttlTimer);
|
|
20181
|
-
held.clear();
|
|
20182
|
-
if (parked > 0 || outstanding > 0) {
|
|
20183
|
-
info(
|
|
20184
|
-
`fanout-ceiling: disposed \u2014 released ${parked} parked task(s) and disarmed ${outstanding} permit timer(s).`
|
|
20185
|
-
);
|
|
20127
|
+
const waiter = waiters.shift();
|
|
20128
|
+
if (waiter === void 0) break;
|
|
20129
|
+
disarm(waiter.timer);
|
|
20130
|
+
waiter.grant();
|
|
20186
20131
|
}
|
|
20132
|
+
for (const slot of pending) disarm(slot.timer);
|
|
20133
|
+
pending.length = 0;
|
|
20134
|
+
for (const child of active.values()) disarm(child.timer);
|
|
20135
|
+
active.clear();
|
|
20187
20136
|
}
|
|
20188
20137
|
return {
|
|
20189
|
-
|
|
20190
|
-
|
|
20191
|
-
|
|
20138
|
+
beforeToolCall,
|
|
20139
|
+
afterToolCall,
|
|
20140
|
+
onSessionCreated,
|
|
20141
|
+
onSessionTerminal,
|
|
20192
20142
|
dispose,
|
|
20193
20143
|
stats: () => ({
|
|
20194
20144
|
limit,
|
|
20195
|
-
|
|
20145
|
+
pending: pending.length,
|
|
20146
|
+
active: active.size,
|
|
20196
20147
|
waiting: waiters.length,
|
|
20197
20148
|
waitedCount,
|
|
20198
20149
|
timedOutCount,
|
|
20199
|
-
|
|
20200
|
-
|
|
20150
|
+
pendingTtlReclaimedCount,
|
|
20151
|
+
activeTtlReclaimedCount,
|
|
20201
20152
|
armedTimers: armed.size
|
|
20202
20153
|
})
|
|
20203
20154
|
};
|
|
20204
20155
|
}
|
|
20205
20156
|
|
|
20206
|
-
// src/hooks/system-
|
|
20207
|
-
import { existsSync as
|
|
20208
|
-
import { join as
|
|
20157
|
+
// src/hooks/system-overlay.ts
|
|
20158
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
20159
|
+
import { join as join3 } from "node:path";
|
|
20209
20160
|
|
|
20210
20161
|
// src/errors.ts
|
|
20211
20162
|
var XemaSystemOverlayMissingError = class extends Error {
|
|
@@ -20218,7 +20169,7 @@ var XemaSystemOverlayMissingError = class extends Error {
|
|
|
20218
20169
|
|
|
20219
20170
|
// src/hooks/workspace-listing.ts
|
|
20220
20171
|
import { readdirSync, statSync } from "node:fs";
|
|
20221
|
-
import { join as
|
|
20172
|
+
import { join as join2 } from "node:path";
|
|
20222
20173
|
var MAX_ENTRIES_PER_DIR = 50;
|
|
20223
20174
|
var HIDDEN_TOP_LEVEL = /* @__PURE__ */ new Set([".xema", ".opencode"]);
|
|
20224
20175
|
var SKIP_CHILD_EXPANSION_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
@@ -20238,7 +20189,7 @@ function renderWorkspaceListing(workspaceRoot) {
|
|
|
20238
20189
|
const lines = [
|
|
20239
20190
|
"## Current workspace contents (depth 1)",
|
|
20240
20191
|
"",
|
|
20241
|
-
"This is a snapshot of `/workspace/` taken at the start of this turn. Use it to orient before reaching for `glob
|
|
20192
|
+
"This is a snapshot of `/workspace/` taken at the start of this turn. Use it to orient before reaching for `glob` or `read`. Hidden: platform-private slots (`.xema/`, `.opencode/`).",
|
|
20242
20193
|
""
|
|
20243
20194
|
];
|
|
20244
20195
|
let topLevel;
|
|
@@ -20260,13 +20211,13 @@ function renderWorkspaceListing(workspaceRoot) {
|
|
|
20260
20211
|
}
|
|
20261
20212
|
function renderTopLevelEntry(workspaceRoot, entry) {
|
|
20262
20213
|
const relPath = entry.name;
|
|
20263
|
-
const absPath =
|
|
20214
|
+
const absPath = join2(workspaceRoot, entry.name);
|
|
20264
20215
|
if (!entry.isDirectory) {
|
|
20265
20216
|
return [`- \`/workspace/${relPath}\``];
|
|
20266
20217
|
}
|
|
20267
20218
|
if (SKIP_CHILD_EXPANSION_TOP_LEVEL.has(entry.name)) {
|
|
20268
20219
|
return [
|
|
20269
|
-
`- \`/workspace/${relPath}/\` _(children omitted: known implicit/noisy directory \u2014 use \`glob
|
|
20220
|
+
`- \`/workspace/${relPath}/\` _(children omitted: known implicit/noisy directory \u2014 use \`glob\` if needed)_`
|
|
20270
20221
|
];
|
|
20271
20222
|
}
|
|
20272
20223
|
let children;
|
|
@@ -20284,7 +20235,7 @@ function renderTopLevelEntry(workspaceRoot, entry) {
|
|
|
20284
20235
|
return ` - \`${child.name}${suffix}\``;
|
|
20285
20236
|
});
|
|
20286
20237
|
const overflow = children.length - shown.length;
|
|
20287
|
-
const overflowLine = overflow > 0 ? [` - _(${overflow} more entr${overflow === 1 ? "y" : "ies"} \u2014 use \`glob
|
|
20238
|
+
const overflowLine = overflow > 0 ? [` - _(${overflow} more entr${overflow === 1 ? "y" : "ies"} \u2014 use \`glob\` to enumerate)_`] : [];
|
|
20288
20239
|
return [`- \`/workspace/${relPath}/\``, ...childLines, ...overflowLine];
|
|
20289
20240
|
}
|
|
20290
20241
|
function readDirSorted(dir) {
|
|
@@ -20310,7 +20261,7 @@ function readDirSorted(dir) {
|
|
|
20310
20261
|
function resolveIsDirectory(parent, dirent) {
|
|
20311
20262
|
if (dirent.isSymbolicLink()) {
|
|
20312
20263
|
try {
|
|
20313
|
-
return statSync(
|
|
20264
|
+
return statSync(join2(parent, dirent.name)).isDirectory();
|
|
20314
20265
|
} catch {
|
|
20315
20266
|
return false;
|
|
20316
20267
|
}
|
|
@@ -20318,114 +20269,155 @@ function resolveIsDirectory(parent, dirent) {
|
|
|
20318
20269
|
return dirent.isDirectory();
|
|
20319
20270
|
}
|
|
20320
20271
|
|
|
20321
|
-
// src/hooks/system-
|
|
20322
|
-
|
|
20323
|
-
|
|
20324
|
-
|
|
20325
|
-
|
|
20326
|
-
"experimental.chat.system.transform"
|
|
20327
|
-
);
|
|
20272
|
+
// src/hooks/system-overlay.ts
|
|
20273
|
+
var XEMA_OVERLAY_METADATA_KEY = "xema.system-overlay";
|
|
20274
|
+
function buildSystemOverlayHook(directory) {
|
|
20275
|
+
return (input2) => {
|
|
20276
|
+
const ctx = readInvocationContextForHook(directory, "session.context");
|
|
20328
20277
|
if (!ctx) {
|
|
20329
20278
|
return;
|
|
20330
20279
|
}
|
|
20331
|
-
const overlayPath =
|
|
20332
|
-
if (!
|
|
20280
|
+
const overlayPath = join3(directory, ".xema", "system-overlay.md");
|
|
20281
|
+
if (!existsSync2(overlayPath)) {
|
|
20333
20282
|
throw new XemaSystemOverlayMissingError(
|
|
20334
|
-
`Xema session ${ctx.invocation.runId ?? "unknown"} (role=${ctx.invocation.role}, sessionId=${ctx.invocation.sessionId ?? "-"}) is missing /workspace/.xema/system-overlay.md. The platform contract is undefined; refusing
|
|
20335
|
-
);
|
|
20336
|
-
}
|
|
20337
|
-
const bytes = readFileSync3(overlayPath, "utf8");
|
|
20338
|
-
if (output2.system.length !== 1) {
|
|
20339
|
-
throw new Error(
|
|
20340
|
-
`experimental.chat.system.transform: expected output.system to contain exactly 1 entry on hook entry, got ${output2.system.length}. Another plugin or an OpenCode change broke the seed-then-extend contract this hook relies on.`
|
|
20283
|
+
`Xema session ${ctx.invocation.runId ?? "unknown"} (role=${ctx.invocation.role}, sessionId=${ctx.invocation.sessionId ?? "-"}) is missing /workspace/.xema/system-overlay.md. The platform contract is undefined; refusing the model request. Verify the agent-run-context render flow emitted the system-overlay slot.`
|
|
20341
20284
|
);
|
|
20342
20285
|
}
|
|
20286
|
+
const bytes = readFileSync2(overlayPath, "utf8");
|
|
20343
20287
|
const listing = renderWorkspaceListing(directory);
|
|
20344
|
-
|
|
20288
|
+
const part = {
|
|
20289
|
+
type: "text",
|
|
20290
|
+
text: `${bytes}
|
|
20345
20291
|
|
|
20346
|
-
${
|
|
20347
|
-
|
|
20348
|
-
|
|
20292
|
+
${listing}`,
|
|
20293
|
+
metadata: { [XEMA_OVERLAY_METADATA_KEY]: true }
|
|
20294
|
+
};
|
|
20295
|
+
const existing = input2.system.findIndex(
|
|
20296
|
+
(entry) => entry.metadata?.[XEMA_OVERLAY_METADATA_KEY] === true
|
|
20297
|
+
);
|
|
20298
|
+
if (existing === -1) {
|
|
20299
|
+
input2.system.push(part);
|
|
20300
|
+
} else {
|
|
20301
|
+
input2.system[existing] = part;
|
|
20302
|
+
}
|
|
20349
20303
|
debug(
|
|
20350
|
-
`
|
|
20304
|
+
`session.context: ${existing === -1 ? "appended" : "replaced"} overlay part (${bytes.length} overlay + ${listing.length} listing bytes, role=${ctx.invocation.role}, opencodeSession=${input2.sessionID})`
|
|
20351
20305
|
);
|
|
20352
20306
|
};
|
|
20353
20307
|
}
|
|
20354
20308
|
|
|
20355
|
-
// src/
|
|
20356
|
-
|
|
20357
|
-
|
|
20358
|
-
|
|
20359
|
-
|
|
20360
|
-
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20364
|
-
|
|
20309
|
+
// src/hooks/tool-surface-observation.ts
|
|
20310
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
20311
|
+
import { join as join4 } from "node:path";
|
|
20312
|
+
var TOOL_SURFACE_FILE = "opencode-tool-surface.json";
|
|
20313
|
+
var DEFAULT_RUNTIME_STATE_DIR = "/run/xema/runtime";
|
|
20314
|
+
var RUNTIME_STATE_DIR_ENV = "XEMA_RUNTIME_STATE_DIR";
|
|
20315
|
+
function resolveRuntimeStateDir(env = process.env) {
|
|
20316
|
+
const declared = env[RUNTIME_STATE_DIR_ENV];
|
|
20317
|
+
return declared !== void 0 && declared.trim() !== "" ? declared : DEFAULT_RUNTIME_STATE_DIR;
|
|
20318
|
+
}
|
|
20319
|
+
async function observeToolSurface(options) {
|
|
20365
20320
|
try {
|
|
20366
|
-
|
|
20321
|
+
const observedAt = (options.now ?? Date.now)();
|
|
20322
|
+
const ids = [...await options.listToolIds()].sort();
|
|
20323
|
+
const doc = { observedAt, tools: ids };
|
|
20324
|
+
mkdirSync(options.stateDir, { recursive: true });
|
|
20325
|
+
const target = join4(options.stateDir, TOOL_SURFACE_FILE);
|
|
20326
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
20327
|
+
writeFileSync(temp, `${JSON.stringify(doc)}
|
|
20328
|
+
`, "utf8");
|
|
20329
|
+
renameSync(temp, target);
|
|
20330
|
+
debug(`tool-surface: observed ${ids.length} tool(s) \u2192 ${target}`);
|
|
20331
|
+
return doc;
|
|
20367
20332
|
} catch (err) {
|
|
20368
|
-
|
|
20369
|
-
|
|
20333
|
+
warn(
|
|
20334
|
+
`tool-surface: observation failed (observation only \u2014 no authority is affected): ` + (err instanceof Error ? err.message : String(err))
|
|
20335
|
+
);
|
|
20336
|
+
return void 0;
|
|
20370
20337
|
}
|
|
20371
|
-
}
|
|
20372
|
-
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
|
|
20338
|
+
}
|
|
20339
|
+
|
|
20340
|
+
// src/plugin-id.ts
|
|
20341
|
+
var XEMA_RUNTIME_BRIDGE_PLUGIN_ID = "xema-runtime-bridge";
|
|
20342
|
+
|
|
20343
|
+
// src/plugin.ts
|
|
20344
|
+
var TOOL_SURFACE_EVENTS = /* @__PURE__ */ new Set([
|
|
20345
|
+
"plugin.updated",
|
|
20346
|
+
"integration.updated",
|
|
20347
|
+
"skill.updated",
|
|
20348
|
+
"agent.updated"
|
|
20349
|
+
]);
|
|
20350
|
+
async function setup(ctx) {
|
|
20351
|
+
const directory = ctx.location.directory;
|
|
20352
|
+
const limit = resolveSubagentFanoutLimit();
|
|
20353
|
+
const fanout = createSubagentFanoutCeiling({ limit });
|
|
20354
|
+
const turnCompletion = createRootTurnCompletion({
|
|
20355
|
+
resolveParentId: async (sessionId) => {
|
|
20356
|
+
const session = await ctx.session.get({ sessionID: sessionId });
|
|
20357
|
+
return session.parentID !== void 0 && session.parentID.length > 0 ? session.parentID : void 0;
|
|
20358
|
+
}
|
|
20359
|
+
});
|
|
20360
|
+
const stateDir = resolveRuntimeStateDir();
|
|
20361
|
+
const observe = () => observeToolSurface({
|
|
20362
|
+
stateDir,
|
|
20363
|
+
listToolIds: async () => (await ctx.tool.list()).map((tool) => tool.id)
|
|
20364
|
+
});
|
|
20365
|
+
const registrations = [
|
|
20366
|
+
await ctx.session.hook("context", buildSystemOverlayHook(directory)),
|
|
20367
|
+
await ctx.session.hook("compaction", buildCompactionInvariantsHook(directory)),
|
|
20368
|
+
await ctx.tool.hook(
|
|
20369
|
+
"execute.before",
|
|
20370
|
+
(input2) => fanout.beforeToolCall({ tool: input2.tool, sessionID: input2.sessionID, id: input2.id })
|
|
20371
|
+
),
|
|
20372
|
+
await ctx.tool.hook("execute.after", (input2) => {
|
|
20373
|
+
fanout.afterToolCall({ tool: input2.tool, sessionID: input2.sessionID, id: input2.id });
|
|
20374
|
+
}),
|
|
20375
|
+
await ctx.shell.hook("create.before", scrubShellEnv)
|
|
20376
|
+
];
|
|
20377
|
+
const abort = new AbortController();
|
|
20378
|
+
const events = (async () => {
|
|
20379
|
+
try {
|
|
20380
|
+
for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
|
|
20381
|
+
handleEvent(event);
|
|
20382
|
+
}
|
|
20383
|
+
} catch (err) {
|
|
20384
|
+
if (!abort.signal.aborted) {
|
|
20385
|
+
warn(
|
|
20386
|
+
`event subscription ended unexpectedly: ${err instanceof Error ? err.message : String(err)}. Fan-out children now release only by TTL and turn-completion notices stop until restart.`
|
|
20387
|
+
);
|
|
20388
|
+
}
|
|
20389
|
+
}
|
|
20390
|
+
})();
|
|
20391
|
+
function handleEvent(event) {
|
|
20392
|
+
const type = event?.type;
|
|
20393
|
+
if (typeof type === "string" && TOOL_SURFACE_EVENTS.has(type)) {
|
|
20394
|
+
void observe();
|
|
20395
|
+
}
|
|
20396
|
+
const parsed = parseRuntimeSessionEvent(event);
|
|
20397
|
+
if (parsed === void 0) return;
|
|
20398
|
+
if (parsed.kind === "created") {
|
|
20399
|
+
fanout.onSessionCreated(parsed.sessionId, parsed.parentId);
|
|
20400
|
+
turnCompletion.onSessionCreated(parsed.sessionId, parsed.parentId);
|
|
20401
|
+
return;
|
|
20402
|
+
}
|
|
20403
|
+
fanout.onSessionTerminal(parsed.sessionId);
|
|
20404
|
+
void turnCompletion.onSessionTerminal(parsed.sessionId, parsed.outcome, parsed.executionId);
|
|
20405
|
+
}
|
|
20406
|
+
await observe();
|
|
20381
20407
|
info(
|
|
20382
|
-
`
|
|
20408
|
+
`setup: ${XEMA_RUNTIME_BRIDGE_PLUGIN_ID} ready (OpenCode ${ctx.app.version}, dir=${directory}, tools=0, sub-agent fan-out limit=${limit}, tool surface \u2192 ${stateDir})`
|
|
20383
20409
|
);
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
// counter that aborts a run already paid for.
|
|
20390
|
-
"tool.execute.before": async (input2) => {
|
|
20391
|
-
await fanout.before(input2);
|
|
20392
|
-
},
|
|
20393
|
-
"tool.execute.after": (input2) => {
|
|
20394
|
-
fanout.after(input2);
|
|
20395
|
-
},
|
|
20396
|
-
// Shutdown. Disarms the ceiling's timers and lets any parked `task` call
|
|
20397
|
-
// through, so a torn-down worker leaves nothing armed and nothing pending.
|
|
20398
|
-
dispose: async () => {
|
|
20399
|
-
fanout.dispose();
|
|
20400
|
-
},
|
|
20401
|
-
// TWO consumers of `session.idle` now, so the channel fans out. opencode
|
|
20402
|
-
// exposes session lifecycle only as a discriminated event union, not as
|
|
20403
|
-
// dedicated hooks, and `Hooks.event` is a single function — so composition
|
|
20404
|
-
// has to happen here.
|
|
20405
|
-
//
|
|
20406
|
-
// The order matters and the isolation matters more: the fan-out sweep is a
|
|
20407
|
-
// LEAK BACKSTOP, so it must run even if auto-commit fails. Auto-commit
|
|
20408
|
-
// already swallows its own errors by hard invariant, but relying on that
|
|
20409
|
-
// from the outside would make this correct only by someone else's promise.
|
|
20410
|
-
event: async (input2) => {
|
|
20411
|
-
const evt = input2.event;
|
|
20412
|
-
if (evt.type === "session.idle") {
|
|
20413
|
-
const sessionId = evt.properties?.sessionID;
|
|
20414
|
-
if (sessionId) {
|
|
20415
|
-
try {
|
|
20416
|
-
fanout.onSessionIdle(sessionId);
|
|
20417
|
-
} catch (err) {
|
|
20418
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
20419
|
-
warn(`fanout-ceiling: session-idle sweep failed: ${message}`);
|
|
20420
|
-
}
|
|
20421
|
-
}
|
|
20422
|
-
}
|
|
20423
|
-
await autocommit(input2);
|
|
20410
|
+
return async () => {
|
|
20411
|
+
abort.abort();
|
|
20412
|
+
fanout.dispose();
|
|
20413
|
+
for (const registration of registrations) {
|
|
20414
|
+
await registration.dispose();
|
|
20424
20415
|
}
|
|
20416
|
+
await events;
|
|
20425
20417
|
};
|
|
20426
|
-
return hooks;
|
|
20427
20418
|
}
|
|
20419
|
+
var plugin = { id: XEMA_RUNTIME_BRIDGE_PLUGIN_ID, setup };
|
|
20420
|
+
var plugin_default = plugin;
|
|
20428
20421
|
export {
|
|
20429
|
-
|
|
20430
|
-
XemaPlugin as default
|
|
20422
|
+
plugin_default as default
|
|
20431
20423
|
};
|