Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions marketplace/plugins/zcode/scripts/lib/review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,15 @@ async function defaultSyncDirectory(path) {
export function extractFinalResult(snapshot, command, turnBoundary = {}) {
const messages = Array.isArray(snapshot?.messages) ? snapshot.messages : [];
const beforeMessageIds = turnBoundary.beforeMessageIds ?? new Set();
const newAssistants = messages.filter((/** @type {any} */ message) => message?.info?.role === 'assistant' && typeof message.info.messageId === 'string' && !beforeMessageIds.has(message.info.messageId));
const linkedAssistants = turnBoundary.inputId ? newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === turnBoundary.inputId) : [];
const assistant = (turnBoundary.inputId ? linkedAssistants : newAssistants).at(-1);
const newAssistants = messages.filter((/** @type {any} */ message) => isAssistantResponse(message, beforeMessageIds));
const directAssistants = turnBoundary.inputId ? newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === turnBoundary.inputId) : [];
let assistant;
if (directAssistants.length) assistant = directAssistants.at(-1);
else if (turnBoundary.inputId) {
const currentUserRoots = messages.filter((/** @type {any} */ message) => isCurrentUserRoot(message, beforeMessageIds));
if (currentUserRoots.length !== 1) throw missingResult();
assistant = newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === currentUserRoots[0].info.messageId).at(-1);
} else assistant = newAssistants.at(-1);
if (['hidden', 'debug'].includes(assistant?.info?.semantics?.uiVisibility)) throw missingResult();
const parts = assistant?.parts?.filter((/** @type {any} */ part) => part?.type === 'text' && part.ignored !== true && typeof part.text === 'string' && part.text.length > 0).map((/** @type {any} */ part) => part.text) ?? [];
if (!parts.length) throw missingResult();
Expand All @@ -341,6 +347,18 @@ export function extractFinalResult(snapshot, command, turnBoundary = {}) {
if (!validateJsonSchema(structured, REVIEW_OUTPUT_SCHEMA)) throw invalidReviewResult();
return `${JSON.stringify(structured, null, 2)}\n`;
}
/** @param {any} message @param {Set<string>} beforeMessageIds */
function isAssistantResponse(message, beforeMessageIds) {
const semantics = message?.info?.semantics;
return message?.info?.role === 'assistant' && typeof message.info.messageId === 'string' && !beforeMessageIds.has(message.info.messageId)
&& (semantics === undefined || semantics.origin === 'agent_runtime' && semantics.kind === 'assistant_response');
}
/** @param {any} message @param {Set<string>} beforeMessageIds */
function isCurrentUserRoot(message, beforeMessageIds) {
const info = message?.info; const semantics = info?.semantics;
return info?.role === 'user' && typeof info.messageId === 'string' && !beforeMessageIds.has(info.messageId) && info.synthetic !== true && info.visibility !== 'model-only' && info.source === undefined
&& (semantics === undefined || semantics.origin === 'real_user' && semantics.kind === 'user_prompt' && semantics.uiVisibility === 'visible');
}
/** @param {any} snapshot */
function snapshotMessageIds(snapshot) { return new Set((Array.isArray(snapshot?.messages) ? snapshot.messages : []).map((/** @type {any} */ message) => message?.info?.messageId).filter((/** @type {unknown} */ value) => typeof value === 'string')); }
function missingResult() { return new PluginError('ZCODE_RESULT_MISSING', 'ZCode completed without a visible result for the current turn.', { category: 'protocol', remedy: 'Inspect the ZCode session and retry.' }); }
Expand Down
4 changes: 2 additions & 2 deletions marketplace/plugins/zcode/scripts/zcode-broker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PluginError } from './lib/errors.mjs';
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from './lib/fs.mjs';
import { isBoundedPublicIdentifier, isSafeIdentifier } from './lib/identifier.mjs';
import { spawnDaemon } from './lib/process.mjs';
import { validSnapshot } from './lib/zcode-schema.mjs';
import { validCreateSnapshot } from './lib/zcode-schema.mjs';
import { BoundedWriter, closeProtocolUntil, connectZCodeBroker, MAX_DRAIN_TIMEOUT_MS, spawnZCodeProtocol } from './lib/zcode-protocol.mjs';
import { resolveWorkspaceStorage } from './lib/workspace.mjs';

Expand Down Expand Up @@ -374,7 +374,7 @@ export class ZCodeBroker {
} catch (error) { if (frame.method === 'session/send') { protocol.abortTurn(frame.params.sessionId); this.settleTurnPermissions(frame.params.sessionId, sendToken); if (this.activeSessionSockets.get(frame.params.sessionId)?.token === sendToken) this.activeSessionSockets.delete(frame.params.sessionId); if (this.admittingSessions.get(frame.params.sessionId) === sendToken) this.admittingSessions.delete(frame.params.sessionId); this.activeSessions.delete(frame.params.sessionId); this.scheduleIdleShutdown(); } if (subscriptionToken && this.pendingConversationTopics.get(frame.params.topic)?.token === subscriptionToken) this.pendingConversationTopics.delete(frame.params.topic); throw error; }
if (frame.method === 'session/create') {
const createdSessionId = result?.session?.sessionId;
if (!isSafeIdentifier(createdSessionId) || typeof requestedSessionId === 'string' && createdSessionId !== requestedSessionId || !validSnapshot(result, createdSessionId, this.options.workspace)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
if (!isSafeIdentifier(createdSessionId) || typeof requestedSessionId === 'string' && createdSessionId !== requestedSessionId || !validCreateSnapshot(result, createdSessionId, this.options.workspace)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
const anonymousCreate = typeof requestedSessionId !== 'string';
if (anonymousCreate && this.sessionOwners.has(createdSessionId)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
if (!this.admission.ownerRequestCurrent(ownerAdmission)) throw brokerInputError();
Expand Down
24 changes: 21 additions & 3 deletions scripts/lib/review.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,15 @@ async function defaultSyncDirectory(path) {
export function extractFinalResult(snapshot, command, turnBoundary = {}) {
const messages = Array.isArray(snapshot?.messages) ? snapshot.messages : [];
const beforeMessageIds = turnBoundary.beforeMessageIds ?? new Set();
const newAssistants = messages.filter((/** @type {any} */ message) => message?.info?.role === 'assistant' && typeof message.info.messageId === 'string' && !beforeMessageIds.has(message.info.messageId));
const linkedAssistants = turnBoundary.inputId ? newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === turnBoundary.inputId) : [];
const assistant = (turnBoundary.inputId ? linkedAssistants : newAssistants).at(-1);
const newAssistants = messages.filter((/** @type {any} */ message) => isAssistantResponse(message, beforeMessageIds));
const directAssistants = turnBoundary.inputId ? newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === turnBoundary.inputId) : [];
let assistant;
if (directAssistants.length) assistant = directAssistants.at(-1);
else if (turnBoundary.inputId) {
const currentUserRoots = messages.filter((/** @type {any} */ message) => isCurrentUserRoot(message, beforeMessageIds));
if (currentUserRoots.length !== 1) throw missingResult();
assistant = newAssistants.filter((/** @type {any} */ message) => message.info.parentMessageId === currentUserRoots[0].info.messageId).at(-1);
} else assistant = newAssistants.at(-1);
if (['hidden', 'debug'].includes(assistant?.info?.semantics?.uiVisibility)) throw missingResult();
const parts = assistant?.parts?.filter((/** @type {any} */ part) => part?.type === 'text' && part.ignored !== true && typeof part.text === 'string' && part.text.length > 0).map((/** @type {any} */ part) => part.text) ?? [];
if (!parts.length) throw missingResult();
Expand All @@ -341,6 +347,18 @@ export function extractFinalResult(snapshot, command, turnBoundary = {}) {
if (!validateJsonSchema(structured, REVIEW_OUTPUT_SCHEMA)) throw invalidReviewResult();
return `${JSON.stringify(structured, null, 2)}\n`;
}
/** @param {any} message @param {Set<string>} beforeMessageIds */
function isAssistantResponse(message, beforeMessageIds) {
const semantics = message?.info?.semantics;
return message?.info?.role === 'assistant' && typeof message.info.messageId === 'string' && !beforeMessageIds.has(message.info.messageId)
&& (semantics === undefined || semantics.origin === 'agent_runtime' && semantics.kind === 'assistant_response');
}
/** @param {any} message @param {Set<string>} beforeMessageIds */
function isCurrentUserRoot(message, beforeMessageIds) {
const info = message?.info; const semantics = info?.semantics;
return info?.role === 'user' && typeof info.messageId === 'string' && !beforeMessageIds.has(info.messageId) && info.synthetic !== true && info.visibility !== 'model-only' && info.source === undefined
&& (semantics === undefined || semantics.origin === 'real_user' && semantics.kind === 'user_prompt' && semantics.uiVisibility === 'visible');
}
/** @param {any} snapshot */
function snapshotMessageIds(snapshot) { return new Set((Array.isArray(snapshot?.messages) ? snapshot.messages : []).map((/** @type {any} */ message) => message?.info?.messageId).filter((/** @type {unknown} */ value) => typeof value === 'string')); }
function missingResult() { return new PluginError('ZCODE_RESULT_MISSING', 'ZCode completed without a visible result for the current turn.', { category: 'protocol', remedy: 'Inspect the ZCode session and retry.' }); }
Expand Down
4 changes: 2 additions & 2 deletions scripts/zcode-broker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PluginError } from './lib/errors.mjs';
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from './lib/fs.mjs';
import { isBoundedPublicIdentifier, isSafeIdentifier } from './lib/identifier.mjs';
import { spawnDaemon } from './lib/process.mjs';
import { validSnapshot } from './lib/zcode-schema.mjs';
import { validCreateSnapshot } from './lib/zcode-schema.mjs';
import { BoundedWriter, closeProtocolUntil, connectZCodeBroker, MAX_DRAIN_TIMEOUT_MS, spawnZCodeProtocol } from './lib/zcode-protocol.mjs';
import { resolveWorkspaceStorage } from './lib/workspace.mjs';

Expand Down Expand Up @@ -374,7 +374,7 @@ export class ZCodeBroker {
} catch (error) { if (frame.method === 'session/send') { protocol.abortTurn(frame.params.sessionId); this.settleTurnPermissions(frame.params.sessionId, sendToken); if (this.activeSessionSockets.get(frame.params.sessionId)?.token === sendToken) this.activeSessionSockets.delete(frame.params.sessionId); if (this.admittingSessions.get(frame.params.sessionId) === sendToken) this.admittingSessions.delete(frame.params.sessionId); this.activeSessions.delete(frame.params.sessionId); this.scheduleIdleShutdown(); } if (subscriptionToken && this.pendingConversationTopics.get(frame.params.topic)?.token === subscriptionToken) this.pendingConversationTopics.delete(frame.params.topic); throw error; }
if (frame.method === 'session/create') {
const createdSessionId = result?.session?.sessionId;
if (!isSafeIdentifier(createdSessionId) || typeof requestedSessionId === 'string' && createdSessionId !== requestedSessionId || !validSnapshot(result, createdSessionId, this.options.workspace)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
if (!isSafeIdentifier(createdSessionId) || typeof requestedSessionId === 'string' && createdSessionId !== requestedSessionId || !validCreateSnapshot(result, createdSessionId, this.options.workspace)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
const anonymousCreate = typeof requestedSessionId !== 'string';
if (anonymousCreate && this.sessionOwners.has(createdSessionId)) { this.clearProtocolGeneration(protocol); throw invalidSessionCreateResult(); }
if (!this.admission.ownerRequestCurrent(ownerAdmission)) throw brokerInputError();
Expand Down
15 changes: 13 additions & 2 deletions tests/fixtures/fake-zcode-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,20 @@ input.on('line', async (line) => {
try { objectiveResult = `authorized:${JSON.parse(encoded)}`; } catch { objectiveResult = 'authorized-objective-missing'; }
}
const session = sessions.get(p.sessionId); if (session) {
const review = /ZCODE_REVIEW_OUTPUT_SCHEMA:\s*\{/i.test(trustedPrompt); const suffix = `turn-${sendCount}`; const inputId = /current unrelated/i.test(p.content) ? 'input-unrelated' : p.inputId;
const review = /ZCODE_REVIEW_OUTPUT_SCHEMA:\s*\{/i.test(trustedPrompt); const suffix = `turn-${sendCount}`;
const linkageMode = /current unrelated/i.test(p.content) ? 'orphan-assistant' : /current distinct id/i.test(p.content) ? 'distinct-user' : 'direct-input';
const inputId = linkageMode === 'direct-input' ? p.inputId : undefined;
if (process.env.FAKE_ZCODE_RECOVERY_CONTROL) { session.messages.push(...messages(p.sessionId, session.settings.model.current)); session.pendingResult = { review, suffix, inputId }; }
else session.messages.push(...resultMessages(p.sessionId, session.settings.model.current, review, suffix, /current hidden/i.test(p.content) ? 'reasoning-only' : undefined, inputId, objectiveResult));
else {
let turnMessages = resultMessages(p.sessionId, session.settings.model.current, review, suffix, /current hidden/i.test(p.content) ? 'reasoning-only' : undefined, inputId, objectiveResult);
if (linkageMode === 'orphan-assistant') turnMessages = turnMessages.slice(1);
if (linkageMode === 'distinct-user') {
turnMessages[0].info.semantics = { origin: 'real_user', kind: 'user_prompt', uiVisibility: 'visible', providerVisibility: 'visible', transcriptVisibility: 'visible' };
turnMessages[1].info.semantics = { origin: 'agent_runtime', kind: 'assistant_response', uiVisibility: 'visible', providerVisibility: 'visible', transcriptVisibility: 'visible' };
if (process.env.FAKE_ZCODE_LINKAGE_RECORD) await writeFile(process.env.FAKE_ZCODE_LINKAGE_RECORD, JSON.stringify({ inputId: p.inputId, userMessageId: turnMessages[0].info.messageId, assistantParentMessageId: turnMessages[1].info.parentMessageId }));
}
session.messages.push(...turnMessages);
}
}
const stateRevision = process.env.FAKE_ZCODE_BARRIER === '1' ? 1000 : 1;
if (session) session.stateRevision = stateRevision;
Expand Down
8 changes: 8 additions & 0 deletions tests/integration/companion.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,14 @@ test('resumed rescue rejects an unrelated-only new assistant result', async () =
assert.equal(jobs.filter((/** @type {any} */ job) => job.status === 'failed').length, 1);
});

test('foreground rescue accepts a 0.16.3 result linked through a distinct user message id', async () => {
const context = await fixture(); const linkageRecord = join(context.directory, 'distinct-linkage.json');
const result = await companion(context, ['rescue', '--fresh', 'current distinct id'], { FAKE_ZCODE_VERSION: '0.16.3', FAKE_ZCODE_GATE_RESULT: 'distinct-id result', FAKE_ZCODE_LINKAGE_RECORD: linkageRecord });
assert.equal(result.code, 0, `${result.stderr}${result.stdout}`); assert.equal(result.json.job.status, 'succeeded'); assert.equal(result.json.result, 'distinct-id result');
const linkage = JSON.parse(await readFile(linkageRecord, 'utf8'));
assert.notEqual(linkage.inputId, linkage.userMessageId); assert.equal(linkage.assistantParentMessageId, linkage.userMessageId);
});

test('foreground launch failure durably fails its reserved job', async () => {
const context = await fixture();
const failed = await companion(context, ['review'], { FAKE_ZCODE_VERSION: '0.1.0' });
Expand Down
Loading
Loading