@remits/remits-cli 0.1.159 → 0.1.161
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/index.js
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
- L4591 Auth And Component Staging
|
|
13
13
|
- L5223 Component Summaries, Status, And Sync Gates
|
|
14
14
|
- L7547 Branches, Promotion, Commit, And Test Runs
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
15
|
+
- L9791 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11308 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L13808 Agent And Ticket Workflows
|
|
18
|
+
- L18069 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -9144,6 +9144,7 @@ function watchWebsocket(baseUrl, topicId, taskId, options = {}) {
|
|
|
9144
9144
|
const emit = options.stderr ? console.error : console.log;
|
|
9145
9145
|
const client = new Client({
|
|
9146
9146
|
brokerURL: webSocketUrl(baseUrl),
|
|
9147
|
+
connectHeaders: { 'X-Auth-Token': options.token || '' },
|
|
9147
9148
|
reconnectDelay: 3000,
|
|
9148
9149
|
heartbeatIncoming: 10000,
|
|
9149
9150
|
heartbeatOutgoing: 10000,
|
|
@@ -9444,7 +9445,7 @@ async function testCommand(flags) {
|
|
|
9444
9445
|
if (flags.watch !== 'false') {
|
|
9445
9446
|
const topic = session.websocketTopic || start.websocketTopic || (session.user && String(session.user.uuid || '').replace(/-/g, ''));
|
|
9446
9447
|
if (topic) {
|
|
9447
|
-
stopWs = watchWebsocket(baseUrl, topic, start.shardTaskIds || start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose) });
|
|
9448
|
+
stopWs = watchWebsocket(baseUrl, topic, start.shardTaskIds || start.taskId, { stderr: jsonOutput, verbose: flagEnabled(flags.verbose), token: session.token });
|
|
9448
9449
|
}
|
|
9449
9450
|
}
|
|
9450
9451
|
|
|
@@ -13548,6 +13549,7 @@ function createManagedWebsocketClient(baseUrl, urlSessions) {
|
|
|
13548
13549
|
},
|
|
13549
13550
|
debug: () => {},
|
|
13550
13551
|
beforeConnect: () => {
|
|
13552
|
+
client.connectHeaders['X-Remits-Tokens'] = JSON.stringify((managedClient.urlSessions || []).map((entry) => entry.token).filter(Boolean));
|
|
13551
13553
|
managedClient.lastConnectAttemptAt = Date.now();
|
|
13552
13554
|
ensureRuntimeWebsocketEntry(normalizedBaseUrl);
|
|
13553
13555
|
runtimeState.websocket[normalizedBaseUrl] = {
|
|
@@ -16717,6 +16719,91 @@ function describeRunLocation(record) {
|
|
|
16717
16719
|
return parts.join(' ');
|
|
16718
16720
|
}
|
|
16719
16721
|
|
|
16722
|
+
function usageListFlag(value) {
|
|
16723
|
+
if (value === undefined || value === null || value === true) return undefined;
|
|
16724
|
+
const items = String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
16725
|
+
return items.length ? items : undefined;
|
|
16726
|
+
}
|
|
16727
|
+
|
|
16728
|
+
async function usageCommand(flags) {
|
|
16729
|
+
const cwd = process.cwd();
|
|
16730
|
+
ensureLocalState(cwd);
|
|
16731
|
+
const sessionContext = resolveSessionContext(cwd, flags);
|
|
16732
|
+
const { session, accountId } = sessionContext;
|
|
16733
|
+
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
16734
|
+
const api = buildAxios(baseUrl, session.token, 120000);
|
|
16735
|
+
const query = {
|
|
16736
|
+
from: flags.from,
|
|
16737
|
+
to: flags.to,
|
|
16738
|
+
timezone: flags.timezone || flags.tz,
|
|
16739
|
+
by: flags.by,
|
|
16740
|
+
groupBy: usageListFlag(flags['group-by'] || flags.groupBy),
|
|
16741
|
+
capabilities: usageListFlag(flags.capabilities || flags.capability),
|
|
16742
|
+
lanes: usageListFlag(flags.lanes || flags.lane),
|
|
16743
|
+
accountId: flags['target-account-id'] || flags.targetAccountId,
|
|
16744
|
+
includeDescendants: flagEnabled(flags['include-descendants']) || undefined,
|
|
16745
|
+
includeSuites: flagEnabled(flags['include-suites']) || undefined,
|
|
16746
|
+
includeMocked: flagEnabled(flags['include-mocked']) || undefined,
|
|
16747
|
+
allocateShared: flagEnabled(flags['allocate-shared']) || undefined,
|
|
16748
|
+
scope: flagEnabled(flags.platform) ? 'platform' : undefined
|
|
16749
|
+
};
|
|
16750
|
+
Object.keys(query).forEach((key) => query[key] === undefined && delete query[key]);
|
|
16751
|
+
const response = await loggedPost(api, cwd, '/cli/usage', {
|
|
16752
|
+
token: session.token,
|
|
16753
|
+
accountId,
|
|
16754
|
+
dataMode: resolveDataMode(flags, session),
|
|
16755
|
+
query
|
|
16756
|
+
}).then((r) => r.data);
|
|
16757
|
+
|
|
16758
|
+
if (!response.success) throw new Error(response.message || 'Usage report failed');
|
|
16759
|
+
if (flagEnabled(flags.json)) {
|
|
16760
|
+
console.log(JSON.stringify(response, null, 2));
|
|
16761
|
+
return response;
|
|
16762
|
+
}
|
|
16763
|
+
printSessionResolutionWarning(sessionContext);
|
|
16764
|
+
printResolvedBaseUrl(baseUrl);
|
|
16765
|
+
printUsageSummary(response);
|
|
16766
|
+
return response;
|
|
16767
|
+
}
|
|
16768
|
+
|
|
16769
|
+
function formatUsageCurrencies(row) {
|
|
16770
|
+
const entries = Object.entries(row.costByCurrency || { USD: row.cost || 0 });
|
|
16771
|
+
return entries.length ? entries.map(([currency, value]) => currency + ' ' + Number(value).toFixed(4)).join(' / ') : 'USD 0.0000';
|
|
16772
|
+
}
|
|
16773
|
+
|
|
16774
|
+
function formatUsageCost(row) {
|
|
16775
|
+
const marker = row.costComplete === false ? ' (floor: ' + (row.unpricedEvents || 0) + ' unpriced)' : '';
|
|
16776
|
+
return formatUsageCurrencies(row) + ' ' + (row.basis || '') + marker;
|
|
16777
|
+
}
|
|
16778
|
+
|
|
16779
|
+
function printUsageSummary(report) {
|
|
16780
|
+
const selector = report.selector || {};
|
|
16781
|
+
console.log('Usage ' + selector.from + ' .. ' + selector.to + ' (' + (selector.timezone || 'UTC') + ', by ' + selector.by + ')');
|
|
16782
|
+
console.log('Total: ' + formatUsageCost(report));
|
|
16783
|
+
const rows = report.rows || [];
|
|
16784
|
+
const names = report.accountNames || {};
|
|
16785
|
+
rows.forEach((row) => {
|
|
16786
|
+
const who = row.accountId !== undefined ? ' account ' + (row.accountId === null ? 'platform' : (names[row.accountId] || row.accountId)) : '';
|
|
16787
|
+
const sku = row.sku !== undefined ? ' [' + (row.sku || '-') + ']' : '';
|
|
16788
|
+
const quantities = Object.entries(row.quantities || {}).map(([unit, value]) => unit + '=' + value).join(' ');
|
|
16789
|
+
console.log(' ' + row.period + ' ' + row.capability + who + sku + ' ' + formatUsageCost(row) + (quantities ? ' ' + quantities : ''));
|
|
16790
|
+
});
|
|
16791
|
+
const excluded = report.excluded || {};
|
|
16792
|
+
const excludedKeys = Object.keys(excluded);
|
|
16793
|
+
if (excludedKeys.length) {
|
|
16794
|
+
console.log('Excluded by the filters: ' + excludedKeys.map((key) => key + ' ' + (excluded[key].events || 0) + ' ops ' + formatUsageCurrencies(excluded[key])).join(', '));
|
|
16795
|
+
}
|
|
16796
|
+
const coverage = report.coverage || {};
|
|
16797
|
+
const partial = Object.keys(coverage).filter((cap) => coverage[cap].partial || coverage[cap].measured === false);
|
|
16798
|
+
if (partial.length) {
|
|
16799
|
+
console.log('Coverage: ' + partial.map((cap) => cap + (coverage[cap].measured === false ? ' not measured yet' : ' from ' + coverage[cap].coverageFrom)).join(', '));
|
|
16800
|
+
}
|
|
16801
|
+
if (report.invoiced && report.invoiced.available) {
|
|
16802
|
+
console.log('Invoiced (allocated provider bill) in range: ' + formatUsageCurrencies(report.invoiced));
|
|
16803
|
+
if (report.invoiced.message) console.log(report.invoiced.message);
|
|
16804
|
+
}
|
|
16805
|
+
}
|
|
16806
|
+
|
|
16720
16807
|
async function activityInspectCommand(flags) {
|
|
16721
16808
|
const cwd = process.cwd();
|
|
16722
16809
|
ensureLocalState(cwd);
|
|
@@ -18597,6 +18684,7 @@ async function main() {
|
|
|
18597
18684
|
console.log(' remits-cli agent map [--account-ids 1,4] [--json] # who is EDITING what, where, right now');
|
|
18598
18685
|
console.log(' remits-cli activity inspect [--account-id ID] [--scope self|children|related] [--workspace NAME] [--workstream ID] [--user-email E] [--summary|--compact] [--json]');
|
|
18599
18686
|
console.log(' remits-cli workstream status [--workstream ID|--workspace NAME] [--json]');
|
|
18687
|
+
console.log(' remits-cli usage --from DATE [--to DATE] [--by hour|day|month] [--group-by account,sku] [--capabilities ai,document_store] [--json]');
|
|
18600
18688
|
console.log(' remits-cli agent release # stop receiving tickets now');
|
|
18601
18689
|
console.log(' remits-cli start [--foreground true] [--port 8787] # human dashboard in the browser');
|
|
18602
18690
|
console.log(' remits-cli stop');
|
|
@@ -18859,6 +18947,23 @@ async function main() {
|
|
|
18859
18947
|
throw new Error('Unknown activity subcommand: ' + subcommand);
|
|
18860
18948
|
}
|
|
18861
18949
|
|
|
18950
|
+
if (command === 'usage') {
|
|
18951
|
+
if (wantsHelp) {
|
|
18952
|
+
console.log('Usage: remits-cli usage --from DATE [--to DATE] [--timezone ZONE] [--by hour|day|month]');
|
|
18953
|
+
console.log(' [--group-by account,sku,segment,provider] [--capabilities ai,document_store,...]');
|
|
18954
|
+
console.log(' [--target-account-id ID] [--include-descendants] [--lanes prod,test|all]');
|
|
18955
|
+
console.log(' [--include-suites] [--include-mocked] [--allocate-shared] [--platform] [--json]');
|
|
18956
|
+
console.log('');
|
|
18957
|
+
console.log('What the account used and cost, by capability: the same answer as front-stage usage([...]).');
|
|
18958
|
+
console.log('Defaults: prod lane, nothing a Test suite caused, nothing mocked; "Excluded" says what that left out.');
|
|
18959
|
+
console.log('Each figure has a basis (invoiced > reported > estimated > metered); a cost marked "floor" has');
|
|
18960
|
+
console.log('unpriced operations. --platform (platform administrators) reports every account and platform usage.');
|
|
18961
|
+
return;
|
|
18962
|
+
}
|
|
18963
|
+
await usageCommand(args);
|
|
18964
|
+
return;
|
|
18965
|
+
}
|
|
18966
|
+
|
|
18862
18967
|
if (command === 'evidence') {
|
|
18863
18968
|
if (wantsHelp) {
|
|
18864
18969
|
console.log('Usage: remits-cli evidence [--limit N] [--json]');
|
package/lib/local-ai.js
CHANGED
|
@@ -66,8 +66,60 @@ function assertTextRequest(request) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// The offered tools and choice rules, derived exactly as LocalAiBridge.decision derives them.
|
|
70
|
+
function offeredTools(request = {}) {
|
|
71
|
+
const legacy = !request.tools?.length && !!request.functions?.length;
|
|
72
|
+
const names = [...new Set((request.tools?.length ? request.tools : request.functions || [])
|
|
73
|
+
.map((tool) => tool?.function?.name ?? tool?.name).filter((name) => typeof name === 'string' && name))];
|
|
74
|
+
const choice = legacy ? request.function_call : request.tool_choice;
|
|
75
|
+
const required = choice && typeof choice === 'object' ? (legacy ? choice.name : choice.function?.name) || null : null;
|
|
76
|
+
return { legacy, names, choice, required };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The transport envelope for THIS packet. Only names and counts are constrained: arguments stay a plain
|
|
80
|
+
// object because tool schemas are repaired forgivingly server-side, and the server still validates all.
|
|
81
|
+
function decisionSchema(request) {
|
|
82
|
+
const { legacy, names, choice, required } = offeredTools(request);
|
|
83
|
+
const allowed = required ? names.filter((name) => name === required) : names;
|
|
84
|
+
let toolCalls;
|
|
85
|
+
if (!allowed.length || choice === 'none') toolCalls = { type: 'array', maxItems: 0 };
|
|
86
|
+
else {
|
|
87
|
+
toolCalls = { type: 'array', items: { type: 'object', additionalProperties: false, required: ['name', 'arguments'],
|
|
88
|
+
properties: { name: { type: 'string', enum: allowed }, arguments: { type: 'object' } } } };
|
|
89
|
+
if (choice === 'required' || required) toolCalls.minItems = 1;
|
|
90
|
+
if (legacy || request.parallel_tool_calls === false) toolCalls.maxItems = 1;
|
|
91
|
+
}
|
|
92
|
+
return { type: 'object', additionalProperties: false, required: ['content', 'toolCalls'],
|
|
93
|
+
properties: { content: { type: ['string', 'null'] }, toolCalls } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// An exact continuation of the previous packet: appended messages, a few same-position replacements
|
|
97
|
+
// (e.g. a regenerated runtime-context message) and changed top-level fields. Null when history was
|
|
98
|
+
// pruned or reordered, when more than MAX_REPLACED messages changed, or when it would not be smaller.
|
|
99
|
+
const MAX_REPLACED = 2;
|
|
100
|
+
function continuation(base, request) {
|
|
101
|
+
if (!base?.request || !Array.isArray(base.request.messages) || !Array.isArray(request?.messages)) return null;
|
|
102
|
+
const previous = base.request.messages;
|
|
103
|
+
if (request.messages.length <= previous.length) return null;
|
|
104
|
+
const replacedMessages = {};
|
|
105
|
+
for (let i = 0; i < previous.length; i++) {
|
|
106
|
+
if (JSON.stringify(previous[i]) === JSON.stringify(request.messages[i])) continue;
|
|
107
|
+
if (previous[i]?.role !== request.messages[i]?.role || Object.keys(replacedMessages).length >= MAX_REPLACED) return null;
|
|
108
|
+
replacedMessages[i] = request.messages[i];
|
|
109
|
+
}
|
|
110
|
+
const changedFields = {};
|
|
111
|
+
for (const key of Object.keys(request)) {
|
|
112
|
+
if (key !== 'messages' && JSON.stringify(request[key]) !== JSON.stringify(base.request[key])) changedFields[key] = request[key];
|
|
113
|
+
}
|
|
114
|
+
const removedFields = Object.keys(base.request).filter((key) => key !== 'messages' && !Object.hasOwn(request, key));
|
|
115
|
+
const delta = { appendedMessages: request.messages.slice(previous.length), changedFields, removedFields };
|
|
116
|
+
if (Object.keys(replacedMessages).length) delta.replacedMessages = replacedMessages;
|
|
117
|
+
return JSON.stringify(delta).length < JSON.stringify(request).length ? delta : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function promptFor(packet, { agent, base } = {}) {
|
|
121
|
+
const delta = base ? continuation(base, packet.request) : null;
|
|
122
|
+
const lines = [
|
|
71
123
|
'You are the receiving model for a Remits AI invocation. Produce ONE next assistant decision.',
|
|
72
124
|
'The JSON packet below is the exact outgoing model request, including system instructions, messages,',
|
|
73
125
|
'tool definitions, tool choice, and response-format requirements. Treat it as authoritative for this turn.',
|
|
@@ -77,16 +129,31 @@ function promptFor(packet) {
|
|
|
77
129
|
'send you a new packet with the results in this SAME conversation. Never fabricate a tool result.',
|
|
78
130
|
'Only call tools offered in THIS packet. Respect tool_choice and parallel_tool_calls.',
|
|
79
131
|
'Earlier packets may have been pruned or corrected. Do not use omitted content as authoritative input.',
|
|
80
|
-
'
|
|
132
|
+
'If a decision you returned earlier is not reflected in this request, Remits did not accept or execute it.'
|
|
133
|
+
];
|
|
134
|
+
if (agent === 'claude') {
|
|
135
|
+
lines.push('Deliver the decision by calling the StructuredOutput tool exactly once with',
|
|
136
|
+
'{"content":"assistant reply or null","toolCalls":[{"name":"offered name","arguments":{}}]}.',
|
|
137
|
+
'StructuredOutput is the Remits transport for this decision, not a workflow tool. Prose, markdown,',
|
|
138
|
+
'or fenced "tool call" examples outside StructuredOutput are discarded and never executed.');
|
|
139
|
+
} else {
|
|
140
|
+
lines.push('Return ONLY one JSON object: {"content":"assistant reply or null","toolCalls":[{"name":"offered name","arguments":{}}]}.');
|
|
141
|
+
}
|
|
142
|
+
lines.push(
|
|
81
143
|
'Use actual JSON null for absent content and [] for no tool calls. If a response_format asks for JSON,',
|
|
82
144
|
'put that formatted response as a JSON-encoded STRING in content, inside this transport envelope.',
|
|
83
145
|
'If the requested reply is empty, return {"content":"","toolCalls":[]} rather than omitting the envelope.',
|
|
84
146
|
'For a content/XML tool channel, put the required tool markup in content and leave toolCalls empty.',
|
|
85
|
-
`Remits sessionId: ${packet.sessionId}; requestId: ${packet.requestId}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
|
|
147
|
+
`Remits sessionId: ${packet.sessionId}; requestId: ${packet.requestId}.`);
|
|
148
|
+
const full = ['<remits_request>', JSON.stringify(packet.request), '</remits_request>'];
|
|
149
|
+
const continued = delta && [`This request CONTINUES request ${base.requestId}, already in this conversation. Reconstruct the exact`,
|
|
150
|
+
'outgoing request: take that request as you reconstructed it, replace the messages at each replacedMessages index',
|
|
151
|
+
'(0-based, if present), append appendedMessages, replace each changedFields entry, and drop removedFields.',
|
|
152
|
+
'Everything else in it is unchanged and still authoritative.',
|
|
153
|
+
'<remits_request_continuation>', JSON.stringify(delta), '</remits_request_continuation>'];
|
|
154
|
+
// A continuation exists only to shrink the turn; short histories are cheaper re-sent whole.
|
|
155
|
+
const chosen = continued && continued.join('\n').length < full.join('\n').length ? continued : full;
|
|
156
|
+
return [...lines, ...chosen].join('\n');
|
|
90
157
|
}
|
|
91
158
|
|
|
92
159
|
function parseDecision(text) {
|
|
@@ -94,6 +161,10 @@ function parseDecision(text) {
|
|
|
94
161
|
let result;
|
|
95
162
|
try { result = JSON.parse(clean); }
|
|
96
163
|
catch (_) { throw new Error('Local agent did not return the decision JSON envelope'); }
|
|
164
|
+
return validateDecision(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateDecision(result) {
|
|
97
168
|
if (!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Local agent decision must be an object');
|
|
98
169
|
if (result.error) return { error: String(result.error).slice(0, 4000) };
|
|
99
170
|
if (result.content != null && typeof result.content !== 'string') throw new Error('Decision content must be a string or null');
|
|
@@ -108,7 +179,9 @@ function parseDecision(text) {
|
|
|
108
179
|
}
|
|
109
180
|
|
|
110
181
|
// Use explicit native IDs, never --last/--continue. Each session has an empty, stable working directory.
|
|
111
|
-
|
|
182
|
+
// Claude's decision is enforced natively: --json-schema offers only the synthetic StructuredOutput tool
|
|
183
|
+
// (verified with --tools '' and --resume), validates it, and returns it as result.structured_output.
|
|
184
|
+
function nativeCommand(agent, nativeSessionId, model, request = {}) {
|
|
112
185
|
let args;
|
|
113
186
|
if (agent === 'codex') {
|
|
114
187
|
args = ['exec', '--sandbox', 'read-only', '--json', '--skip-git-repo-check'];
|
|
@@ -117,7 +190,8 @@ function nativeCommand(agent, nativeSessionId, model) {
|
|
|
117
190
|
args.push('-');
|
|
118
191
|
} else if (agent === 'claude') {
|
|
119
192
|
args = ['-p', '--output-format', 'stream-json', '--verbose', '--tools', '',
|
|
120
|
-
'--strict-mcp-config', '--disable-slash-commands', '--setting-sources', ''
|
|
193
|
+
'--strict-mcp-config', '--disable-slash-commands', '--setting-sources', '',
|
|
194
|
+
'--json-schema', JSON.stringify(decisionSchema(request))];
|
|
121
195
|
if (nativeSessionId) args.push('--resume', nativeSessionId);
|
|
122
196
|
if (model) args.push('--model', model);
|
|
123
197
|
} else if (agent === 'gemini') {
|
|
@@ -136,6 +210,7 @@ function consumeEvent(agent, event, state) {
|
|
|
136
210
|
identifier(id);
|
|
137
211
|
if (state.nativeSessionId && state.nativeSessionId !== id) throw new Error('Native agent changed session ID; refusing to fork the Remits session');
|
|
138
212
|
state.nativeSessionId = id;
|
|
213
|
+
state.started = true;
|
|
139
214
|
}
|
|
140
215
|
if (event.model) state.model = String(event.model);
|
|
141
216
|
if (agent === 'codex') {
|
|
@@ -143,6 +218,16 @@ function consumeEvent(agent, event, state) {
|
|
|
143
218
|
if (event.type === 'turn.completed') state.completed = true;
|
|
144
219
|
if (event.type === 'turn.failed' || event.type === 'error') state.error = event.error?.message || event.message || 'Codex turn failed';
|
|
145
220
|
} else if (agent === 'claude') {
|
|
221
|
+
if (event.type === 'system' && event.subtype === 'init') {
|
|
222
|
+
if (event.claude_code_version) state.version = String(event.claude_code_version).slice(0, 40);
|
|
223
|
+
if (Array.isArray(event.tools) && !event.tools.includes('StructuredOutput')) {
|
|
224
|
+
const error = new Error(`Claude Code ${state.version || ''} did not offer StructuredOutput for --json-schema; upgrade Claude Code. No decision was requested.`);
|
|
225
|
+
error.notStarted = true;
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Native history was summarized: the next turn must not rely on an earlier packet being verbatim.
|
|
230
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary') state.compacted = true;
|
|
146
231
|
if (event.type === 'assistant') {
|
|
147
232
|
if (event.message?.model) state.model = event.message.model;
|
|
148
233
|
const content = (event.message?.content || []).filter((part) => part.type === 'text').map((part) => part.text).join('');
|
|
@@ -150,8 +235,16 @@ function consumeEvent(agent, event, state) {
|
|
|
150
235
|
}
|
|
151
236
|
if (event.type === 'result') {
|
|
152
237
|
state.completed = true;
|
|
153
|
-
|
|
154
|
-
if (event.
|
|
238
|
+
// Only the schema-validated structured result is a decision; result text is never parsed for one.
|
|
239
|
+
if (event.structured_output != null) state.decision = event.structured_output;
|
|
240
|
+
state.text = event.result || state.text;
|
|
241
|
+
state.outcome = { subtype: event.subtype, turns: event.num_turns, stop: event.stop_reason };
|
|
242
|
+
if (event.is_error || (event.subtype && event.subtype !== 'success')) {
|
|
243
|
+
state.error = (event.errors || []).join('; ') || (event.is_error && event.result) || event.subtype;
|
|
244
|
+
if (event.subtype === 'error_max_structured_output_retries') {
|
|
245
|
+
state.error = 'Claude exhausted its structured-output retries without a valid decision envelope';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
155
248
|
}
|
|
156
249
|
} else if (agent === 'gemini') {
|
|
157
250
|
if (event.type === 'message' && event.role === 'assistant') state.text = (state.text || '') + (event.content || '');
|
|
@@ -162,9 +255,14 @@ function consumeEvent(agent, event, state) {
|
|
|
162
255
|
}
|
|
163
256
|
}
|
|
164
257
|
|
|
165
|
-
|
|
258
|
+
// Packet continuations are limited to adapters whose native history compaction is observable.
|
|
259
|
+
const CONTINUATION_AGENTS = new Set(['claude']);
|
|
260
|
+
|
|
261
|
+
// A rejection carries what the supervisor needs to settle the session: notStarted (no native turn
|
|
262
|
+
// began, so nothing is uncertain) or completedTurn (the native turn settled; only its decision failed).
|
|
263
|
+
function runNative({ agent, nativeSessionId, model, cwd, packet, base, signal, onSession, onProcess }, spawnFn = spawn) {
|
|
166
264
|
assertTextRequest(packet.request);
|
|
167
|
-
const command = nativeCommand(agent, nativeSessionId, model);
|
|
265
|
+
const command = nativeCommand(agent, nativeSessionId, model, packet.request);
|
|
168
266
|
return new Promise((resolve, reject) => {
|
|
169
267
|
const env = { ...process.env };
|
|
170
268
|
// Authentication for Remits stays in the supervisor. Native CLIs retain their own login/environment.
|
|
@@ -175,7 +273,7 @@ function runNative({ agent, nativeSessionId, model, cwd, packet, signal, onSessi
|
|
|
175
273
|
const state = { nativeSessionId, text: '', completed: false };
|
|
176
274
|
let bytes = 0, buffer = '', stderr = '', fatal, killTimer;
|
|
177
275
|
const kill = (reason) => {
|
|
178
|
-
if (!fatal) fatal = new Error(reason);
|
|
276
|
+
if (!fatal) fatal = reason instanceof Error ? reason : new Error(reason);
|
|
179
277
|
const send = (sig) => {
|
|
180
278
|
try { process.platform === 'win32' ? child.kill(sig) : process.kill(-child.pid, sig); } catch (_) { /* exited */ }
|
|
181
279
|
};
|
|
@@ -191,7 +289,7 @@ function runNative({ agent, nativeSessionId, model, cwd, packet, signal, onSessi
|
|
|
191
289
|
const previous = state.nativeSessionId;
|
|
192
290
|
consumeEvent(agent, event, state);
|
|
193
291
|
if (state.nativeSessionId && state.nativeSessionId !== previous) onSession?.(state.nativeSessionId);
|
|
194
|
-
} catch (error) { kill(error
|
|
292
|
+
} catch (error) { kill(error); }
|
|
195
293
|
};
|
|
196
294
|
child.stdout.setEncoding('utf8');
|
|
197
295
|
child.stderr.setEncoding('utf8');
|
|
@@ -214,22 +312,48 @@ function runNative({ agent, nativeSessionId, model, cwd, packet, signal, onSessi
|
|
|
214
312
|
handleLine(buffer);
|
|
215
313
|
clearTimeout(killTimer);
|
|
216
314
|
try { onProcess?.(null); } catch (error) { fatal = error; }
|
|
217
|
-
|
|
218
|
-
|
|
315
|
+
const settled = !fatal && code === 0 && state.completed && !!state.nativeSessionId;
|
|
316
|
+
const fail = (error) => {
|
|
317
|
+
if (!state.started) error.notStarted = true;
|
|
318
|
+
if (settled) error.completedTurn = true;
|
|
319
|
+
reject(error);
|
|
320
|
+
};
|
|
321
|
+
if (!settled || state.error) {
|
|
322
|
+
const option = code !== 0 && /unknown option '?(--[\w-]+)/i.exec(stderr);
|
|
323
|
+
fail(fatal || new Error(option ? `Installed ${agent} CLI does not support ${option[1]}; upgrade it. No native turn ran.` :
|
|
324
|
+
state.error ? `Native ${agent} turn failed: ${String(state.error).slice(0, 1000)}${describe(state)}` :
|
|
325
|
+
`Native ${agent} failed (exit ${code}; completed=${state.completed}; session=${!!state.nativeSessionId}). ${stderr}`));
|
|
219
326
|
return;
|
|
220
327
|
}
|
|
221
|
-
try {
|
|
222
|
-
|
|
328
|
+
try {
|
|
329
|
+
let decision;
|
|
330
|
+
if (agent === 'claude') {
|
|
331
|
+
if (state.decision == null) {
|
|
332
|
+
throw new Error('Claude completed without a structured decision: StructuredOutput was not called, so its ' +
|
|
333
|
+
`${String(state.text || '').length}-character reply was neither parsed nor executed${describe(state)}`);
|
|
334
|
+
}
|
|
335
|
+
decision = validateDecision(state.decision);
|
|
336
|
+
} else decision = parseDecision(state.text);
|
|
337
|
+
resolve({ ...decision, nativeSessionId: state.nativeSessionId, model: state.model || model || null, compacted: !!state.compacted });
|
|
338
|
+
} catch (error) { fail(error); }
|
|
223
339
|
});
|
|
224
340
|
try {
|
|
225
341
|
onProcess?.(child.pid);
|
|
226
342
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
227
343
|
if (signal?.aborted) onAbort();
|
|
228
|
-
child.stdin.end(promptFor(packet));
|
|
344
|
+
child.stdin.end(promptFor(packet, { agent, base: CONTINUATION_AGENTS.has(agent) ? base : null }));
|
|
229
345
|
} catch (error) { kill(error.message); }
|
|
230
346
|
});
|
|
231
347
|
}
|
|
232
348
|
|
|
349
|
+
// Bounded native diagnostics: identifiers and counts only, never packet or reply content.
|
|
350
|
+
function describe(state) {
|
|
351
|
+
const parts = [state.version && 'native ' + state.version, state.model && 'model ' + state.model,
|
|
352
|
+
state.outcome?.subtype && 'result ' + state.outcome.subtype, state.outcome?.turns != null && 'turns ' + state.outcome.turns,
|
|
353
|
+
state.outcome?.stop && 'stop ' + state.outcome.stop].filter(Boolean);
|
|
354
|
+
return parts.length ? ` (${parts.join('; ')})` : '';
|
|
355
|
+
}
|
|
356
|
+
|
|
233
357
|
class Supervisor {
|
|
234
358
|
constructor({ api, dir, worker, agent, model, log = console.error, run = runNative, clock = Date.now, onStop }) {
|
|
235
359
|
Object.assign(this, { api, dir, worker, agent, model, log, run, clock, onStop });
|
|
@@ -310,23 +434,27 @@ class Supervisor {
|
|
|
310
434
|
} else {
|
|
311
435
|
const cwd = path.join(this.dir, 'workspaces', sessionId);
|
|
312
436
|
mkdir(cwd);
|
|
437
|
+
const base = this.continuationBase(session);
|
|
438
|
+
const prior = { state: session.state, requestId: session.requestId, nativeSessionId: session.nativeSessionId };
|
|
313
439
|
cached = { packet, state: 'running' };
|
|
314
440
|
write(requestFile, cached);
|
|
315
441
|
Object.assign(session, { state: 'running', requestId: id, updatedAt: this.clock() });
|
|
316
442
|
write(sessionFile, session);
|
|
317
443
|
const timeout = setTimeout(() => task.controller.abort(), Math.max(1, packet.expiresAt - this.clock()));
|
|
318
444
|
try {
|
|
319
|
-
|
|
320
|
-
agent: this.agent, nativeSessionId: session.nativeSessionId, model: this.model, cwd, packet,
|
|
445
|
+
const { compacted, ...decision } = await this.run({
|
|
446
|
+
agent: this.agent, nativeSessionId: session.nativeSessionId, model: this.model, cwd, packet, base,
|
|
321
447
|
signal: task.controller.signal,
|
|
322
448
|
onSession: (nativeSessionId) => { session.nativeSessionId = nativeSessionId; write(sessionFile, session); },
|
|
323
449
|
onProcess: (pid) => { session.pid = pid; write(sessionFile, session); }
|
|
324
450
|
});
|
|
325
|
-
|
|
326
|
-
session.state
|
|
451
|
+
answer = decision;
|
|
452
|
+
Object.assign(session, { nativeSessionId: answer.nativeSessionId, state: 'ready', refreshPacket: !!compacted });
|
|
327
453
|
this.log(`Local AI ${id}: ${this.agent} session ${session.nativeSessionId} → Remits ${sessionId}`);
|
|
328
454
|
} catch (error) {
|
|
329
|
-
|
|
455
|
+
if (error.notStarted) Object.assign(session, prior); // Nothing reached the native conversation.
|
|
456
|
+
else if (error.completedTurn) session.state = 'ready'; // Settled turn; its rejected decision is never a base.
|
|
457
|
+
else session.state = 'uncertain';
|
|
330
458
|
answer = { error: error.message.slice(0, 4000), nativeSessionId: session.nativeSessionId };
|
|
331
459
|
} finally { clearTimeout(timeout); }
|
|
332
460
|
// Write the response first; after a crash it can be delivered without another inference.
|
|
@@ -352,6 +480,16 @@ class Supervisor {
|
|
|
352
480
|
}
|
|
353
481
|
}
|
|
354
482
|
|
|
483
|
+
// The previous packet may be sent as a prefix only when its decision was delivered from this same
|
|
484
|
+
// native conversation and native history has not been compacted since.
|
|
485
|
+
continuationBase(session) {
|
|
486
|
+
if (!session.nativeSessionId || !session.requestId || session.refreshPacket || session.state !== 'ready') return null;
|
|
487
|
+
const prior = read(path.join(this.dir, 'requests', identifier(session.requestId) + '.json'));
|
|
488
|
+
if (prior?.state !== 'delivered' || !prior.packet?.request || !prior.response || prior.response.error ||
|
|
489
|
+
prior.response.nativeSessionId !== session.nativeSessionId) return null;
|
|
490
|
+
return { requestId: session.requestId, request: prior.packet.request };
|
|
491
|
+
}
|
|
492
|
+
|
|
355
493
|
async stop() {
|
|
356
494
|
this.stopping = true;
|
|
357
495
|
for (const task of this.active.values()) task.controller.abort();
|
|
@@ -434,7 +572,10 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
434
572
|
webSocketFactory: () => new WebSocket(baseUrl.replace(/^http/, 'ws').replace(/\/+$/, '') + '/stomp'),
|
|
435
573
|
reconnectDelay: 5000, heartbeatIncoming: 10000, heartbeatOutgoing: 10000,
|
|
436
574
|
connectionTimeout: 15000, discardWebsocketOnCommFailure: true, debug: () => {},
|
|
437
|
-
|
|
575
|
+
// The STOMP connection has no HTTP session cookie. Use the same credential as polling;
|
|
576
|
+
// diagnostic user IDs are not authentication. STOMP resends these on every reconnect.
|
|
577
|
+
connectHeaders: { 'X-Auth-Token': session.token,
|
|
578
|
+
'x-remits-ws-label': 'remits-cli-ai', 'x-remits-user-id': String(session.user.id) },
|
|
438
579
|
onConnect: () => {
|
|
439
580
|
socket.subscribe('/topic/messages/' + topic, (message) => {
|
|
440
581
|
try {
|
|
@@ -462,4 +603,4 @@ async function command({ subcommand, flags, context, root, axios, Client, WebSoc
|
|
|
462
603
|
}
|
|
463
604
|
|
|
464
605
|
module.exports = { command, help, Supervisor, runNative, nativeCommand, consumeEvent, promptFor, parseDecision,
|
|
465
|
-
assertTextRequest, stateDirectory, acquireLock, read, write, identifier };
|
|
606
|
+
validateDecision, decisionSchema, offeredTools, continuation, assertTextRequest, stateDirectory, acquireLock, read, write, identifier };
|
package/package.json
CHANGED
|
@@ -29,6 +29,13 @@ and a precise diagnosis.
|
|
|
29
29
|
|
|
30
30
|
### The three source layers + the compile cache
|
|
31
31
|
|
|
32
|
+
CLI global Tools resolve staged edits in the Tool owner's account using the caller's user,
|
|
33
|
+
git branch and workspace, even when `--as-account` selects another execution account.
|
|
34
|
+
Stage the Tool in its owner repository and dependent components in their own repository with
|
|
35
|
+
the same workspace. Check the Tool's `componentSource` and `componentSignature` separately
|
|
36
|
+
from the Action's returned provenance. This requires the platform correction tracked as 68570;
|
|
37
|
+
older builds use the committed global Tool when it is outside the execution account's inheritance chain.
|
|
38
|
+
|
|
32
39
|
1. **CLI staging cache (Redis, 240-min TTL).** Branch + user + account scoped overrides written by
|
|
33
40
|
`remits-cli components stage`. These shadow the layers below **only during CLI/test-mode execution**
|
|
34
41
|
(see "When staged overrides apply" below).
|
|
@@ -18,7 +18,8 @@ remits-cli ai serve --agent codex --base-url http://localhost:8080 --account-id
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Use `claude`, `gemini`, or `manual` instead of `codex`. Native executables must be installed and logged
|
|
21
|
-
in.
|
|
21
|
+
in. Claude decisions are enforced with Claude Code's `--json-schema` structured output; an installed
|
|
22
|
+
Claude Code that lacks it fails each request with an "upgrade Claude Code" error before any native turn. `--model` selects the native model; the workflow's `model` remains its configured provider target.
|
|
22
23
|
`--max-concurrent 1..16` controls concurrent Remits sessions; default 1. `--worker-id` selects a named
|
|
23
24
|
worker. `--role` declares a role when the user's agent permissions require one. Workstream permission
|
|
24
25
|
is `ai`.
|
|
@@ -88,8 +89,11 @@ supervisor observes release and exits. No automatic migration to a different wor
|
|
|
88
89
|
|
|
89
90
|
Missed WebSocket notifications recover through authenticated polling. Finished decisions are saved
|
|
90
91
|
before submission, so an HTTP retry does not repeat inference. If the supervisor died during inference,
|
|
91
|
-
the native outcome is uncertain: inspect the native session and use
|
|
92
|
-
deleting the journal and accidentally replaying a tool decision.
|
|
92
|
+
or a native turn was killed mid-way, the native outcome is uncertain: inspect the native session and use
|
|
93
|
+
a new Remits session rather than deleting the journal and accidentally replaying a tool decision. A native
|
|
94
|
+
turn that finished with an invalid decision (for Claude, a reply that never used structured output) fails
|
|
95
|
+
that request only; the session stays usable and the next request re-sends the full packet. A native CLI
|
|
96
|
+
that failed before starting a turn leaves the session as it was. Never run competing supervisors with
|
|
93
97
|
the same worker ID.
|
|
94
98
|
|
|
95
99
|
Local state is under `~/.remits-cli/ai/<scope hash>/`: `worker.json`, `sessions/`, `requests/`, and isolated
|
|
@@ -529,7 +529,7 @@ turns up a run that is consuming resources and should not finish — the case th
|
|
|
529
529
|
# the usual path: you found the event in mcp_record_listing / mcp_object_activity
|
|
530
530
|
remits-cli tool --account-id 21 --as-account 49 --target-account 49 --name mcp_run_action --data-mode prod --input '{"controlAction":"interrupt","eventId":19102,"reason":"runaway extraction, 45min"}'
|
|
531
531
|
|
|
532
|
-
# or stop
|
|
532
|
+
# or stop an event-backed or managed async run you started yourself
|
|
533
533
|
remits-cli tool --account-id 21 --as-account 49 --target-account 49 --name mcp_run_action --data-mode prod --input '{"controlAction":"interrupt","actionRunId":"my-run-id"}'
|
|
534
534
|
```
|
|
535
535
|
|
|
@@ -542,7 +542,7 @@ Aliases `cancel` / `stop` / `kill` all work. What you need to know before using
|
|
|
542
542
|
- **Work already committed is NOT rolled back.** This stops further work; it does not undo what has run.
|
|
543
543
|
- **Only `PENDING`/`QUEUED`/`PROCESSING` can be interrupted.** A terminal event is reported back with its
|
|
544
544
|
status rather than being silently reported as "interrupted".
|
|
545
|
-
- **
|
|
545
|
+
- **Async runs also support cooperative stop** once the updated platform and System Account Tool 132 are deployed. `interruptRequested:true` means the stop was recorded; poll until the worker reports a terminal status. Legacy async runs without `runControlId`, and direct runs, cannot be stopped through this handle. An explicit server shutdown settles managed async runs as failed with `terminationReason:owner_shutdown`; inspect partial effects before any rerun.
|
|
546
546
|
- Tenant-scoped: you cannot interrupt another account's event.
|
|
547
547
|
- The response returns `previousStatus`, `eventStatus`, and `threadGroupingId`, so you can pivot straight
|
|
548
548
|
into `mcp_performance_trace` / `mcp_system_logs` to see what it was doing when you stopped it.
|