mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
fix(harness): harden V4 coordination and mobile overflow
Preserve durable collaboration receipts and delivery state, observe background terminal failures, expose complete validated V4 phases and Human Gate actions, and refine message controls plus animated return-to-bottom behavior on narrow layouts.\n\nChecks: frontend 300/300; desktop 1288/1288; deterministic eval 6 cases/18 results; web and desktop renderer builds; git diff --check.
This commit is contained in:
1 parent
9b5a79451e
commit
575fb21fa6
22 files changed
+535
-88
No files matched your search
+1
-1
@@ -24,7 +24,7 @@
|
||||
- Shows real collaboration phases for parallel divergence, cross-Agent challenge and negotiation, responsibility-based work, convergence, and independent verification.
|
||||
- Adds group-scoped unlimited-mode confirmation and state-driven motion with keyboard, Escape, `aria-live`, narrow-layout, and reduced-motion support.
|
||||
- Adds an animated return-to-bottom control to group and direct conversation timelines.
|
||||
- Moves Agent message copy actions into the compact footer action row and hardens long-name, timestamp, and narrow-screen layout behavior.
|
||||
- Keeps Agent message copy actions in the metadata row, aligns version switching, regenerate, and delete controls in the compact footer row, and hardens long-name, timestamp, and narrow-screen layout behavior.
|
||||
- Keeps legacy round-zero trace labels neutral instead of relabeling historical responses as concurrent work.
|
||||
|
||||
### Durability And Safety
|
||||
|
||||
@@ -618,8 +618,7 @@ function resolveCoordinationConsensus(input = {}) {
|
||||
const agentKind = cleanId(receipt?.agentKind)
|
||||
if (!agentKind || !targetKinds.includes(agentKind) || supportsByKind.has(agentKind)
|
||||
|| receipt?.phase !== 'challenge'
|
||||
|| !['support', 'contradict'].includes(verdict)
|
||||
|| (receipt?.agreeToPlan === true && verdict !== 'support')) {
|
||||
|| !['support', 'contradict'].includes(verdict)) {
|
||||
fail('ORCHESTRATION_V4_COORDINATION_PLAN_INVALID')
|
||||
}
|
||||
let candidate = null
|
||||
@@ -1532,6 +1531,7 @@ function validateV4Relations(orchestration, targetKinds) {
|
||||
item?.receipt?.receiptId === workReceipt.collaborationReceipt.receiptId
|
||||
))?.receipt
|
||||
const operationBound = slot?.operationId === workReceipt.operationId
|
||||
|| historicalReceipt?.operationId === workReceipt.operationId
|
||||
|| orchestration.deliveryWatermarks.some(watermark => (
|
||||
watermark.agentKind === workReceipt.ownerKind
|
||||
&& watermark.phase === 'work'
|
||||
@@ -2361,7 +2361,7 @@ function buildCollaborationPackage(receipts, options = {}) {
|
||||
if (!Array.isArray(receipts) || receipts.length > MAX_V4_SLOTS * 8) {
|
||||
fail('COLLABORATION_PACKAGE_INVALID')
|
||||
}
|
||||
const parsed = receipts.map(receipt => parseCollaborationControlBlock(receipt, options))
|
||||
const parsed = receipts.map(receipt => parseCollaborationControlBlock(receipt))
|
||||
.filter(Boolean)
|
||||
const latest = new Map()
|
||||
for (const receipt of parsed) {
|
||||
|
||||
@@ -1629,7 +1629,8 @@ class LocalWorkspaceAutoRunner {
|
||||
}
|
||||
|
||||
v4CoordinationResult({
|
||||
receiptRecords, targetKinds, snapshotHash, challengeBindings = [], requireSupport = true,
|
||||
receiptRecords, targetKinds, snapshotHash, snapshot = null,
|
||||
challengeBindings = [], requireSupport = true,
|
||||
}) {
|
||||
const candidateReceipts = receiptRecords
|
||||
.filter(record => record?.receipt?.phase === 'challenge')
|
||||
@@ -1644,13 +1645,35 @@ class LocalWorkspaceAutoRunner {
|
||||
})
|
||||
: []
|
||||
if (supportReceipts.some(receipt => !receipt)) {
|
||||
return resolveCoordinationConsensus({
|
||||
const result = resolveCoordinationConsensus({
|
||||
targetKinds, snapshotHash, candidateReceipts, supportReceipts: [],
|
||||
})
|
||||
return this.v4SemanticCoordinationResult(result, snapshot)
|
||||
}
|
||||
return resolveCoordinationConsensus({
|
||||
const result = resolveCoordinationConsensus({
|
||||
targetKinds, snapshotHash, candidateReceipts, supportReceipts,
|
||||
})
|
||||
return this.v4SemanticCoordinationResult(result, snapshot)
|
||||
}
|
||||
|
||||
v4SemanticCoordinationResult(result, snapshot) {
|
||||
const candidates = result.candidates.filter((candidate) => {
|
||||
try {
|
||||
for (const assignment of candidate.assignments) {
|
||||
this.v4DirectAssignmentReferences(assignment, snapshot)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
if (error?.message === 'LOCAL_RUN_V4_ASSIGNMENT_REFERENCE_INVALID') return false
|
||||
throw error
|
||||
}
|
||||
})
|
||||
const candidateHashes = new Set(candidates.map(candidate => candidate.planHash))
|
||||
return {
|
||||
...result,
|
||||
plan: result.plan && candidateHashes.has(result.plan.planHash) ? result.plan : null,
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
v4SanitizeDeliveryText(value, limit = 6000) {
|
||||
@@ -1812,11 +1835,7 @@ class LocalWorkspaceAutoRunner {
|
||||
return owned[0] || null
|
||||
}
|
||||
|
||||
v4WorkAssignmentText(
|
||||
coordinationPlan, kind, taskId, snapshot = null, limit = 6000, receiptRecords = [],
|
||||
) {
|
||||
const assignment = this.v4CoordinationAssignment(coordinationPlan, kind, { taskId })
|
||||
if (!assignment) throw new Error('LOCAL_RUN_V4_COORDINATION_PLAN_INVALID')
|
||||
v4DirectAssignmentReferences(assignment, snapshot) {
|
||||
const snapshotSources = new Map([
|
||||
[snapshot?.messageId, snapshot?.taskText],
|
||||
...(Array.isArray(snapshot?.history) ? snapshot.history : []).map(item => [item?.id, item?.text]),
|
||||
@@ -1851,6 +1870,17 @@ class LocalWorkspaceAutoRunner {
|
||||
body: safeReferenceText(identity.content),
|
||||
}
|
||||
})
|
||||
return { inputReferences, directArtifacts }
|
||||
}
|
||||
|
||||
v4WorkAssignmentText(
|
||||
coordinationPlan, kind, taskId, snapshot = null, limit = 6000, receiptRecords = [],
|
||||
) {
|
||||
const assignment = this.v4CoordinationAssignment(coordinationPlan, kind, { taskId })
|
||||
if (!assignment) throw new Error('LOCAL_RUN_V4_COORDINATION_PLAN_INVALID')
|
||||
const { inputReferences, directArtifacts } = this.v4DirectAssignmentReferences(
|
||||
assignment, snapshot,
|
||||
)
|
||||
const directReferences = [...inputReferences, ...directArtifacts]
|
||||
const dependencies = new Set(assignment.dependsOn)
|
||||
const latestDependencies = new Map()
|
||||
@@ -4906,6 +4936,7 @@ class LocalWorkspaceAutoRunner {
|
||||
receiptRecords,
|
||||
targetKinds,
|
||||
snapshotHash,
|
||||
snapshot,
|
||||
requireSupport: false,
|
||||
})
|
||||
const challenge = await runPhase('challenge', targetKinds, challengeBindings, {
|
||||
@@ -4918,6 +4949,7 @@ class LocalWorkspaceAutoRunner {
|
||||
receiptRecords,
|
||||
targetKinds,
|
||||
snapshotHash,
|
||||
snapshot,
|
||||
challengeBindings,
|
||||
})
|
||||
if (!coordination.plan) {
|
||||
@@ -5526,6 +5558,7 @@ class LocalWorkspaceAutoRunner {
|
||||
}
|
||||
})()
|
||||
controller.promise = promise
|
||||
promise.catch(() => {})
|
||||
return controller
|
||||
}
|
||||
|
||||
|
||||
@@ -230,15 +230,15 @@ function v4Prompt({
|
||||
const requiredVerifierCount = Math.min(2, Math.max(0, selectedAgentCount - 1))
|
||||
const phaseInstruction = {
|
||||
proposal: 'Develop an independent proposal. State at least one capability, intended work item, and Artifact you will deliver, plus an explicit dependencies array (which may be empty). Do not rely on another Agent output from this batch.',
|
||||
challenge: `Discuss the proposals as peers and negotiate one shared responsibility graph. Every selected Agent must own at least one substantive work package, and an Agent may own multiple dependent work packages. The graph must name exactly ${requiredVerifierCount} distinct verifierKinds from the selected Agents and must exclude finalizerKind. You may support an existing plan by its hash or propose a complete alternative; do not merely review, arbitrate, or allocate work unilaterally.`,
|
||||
challenge: `Discuss the proposals as peers and negotiate one shared responsibility graph. Every selected Agent must own at least one substantive work package, and an Agent may own multiple dependent work packages. The graph must include one finalizerKind owned by an integrator. It must name exactly ${requiredVerifierCount} distinct verifierKinds from the selected Agents; verifierKinds must not contain finalizerKind. taskId values must be unique; dependsOn may reference only other taskId values and must remain acyclic. inputRefs are exact displayed frozen Source IDs; artifactIds are existing immutable Artifact IDs; future outputs flow only through dependsOn. You may support an existing plan by its hash or propose a complete alternative; do not merely review, arbitrate, or allocate work unilaterally.`,
|
||||
work: 'Execute the agreed responsibility assigned to you. Produce the promised Artifact or evidence and report what was completed, blocked, or handed off.',
|
||||
synthesis: 'Assemble the agreed work products into one bounded deliverable for the user.',
|
||||
verification: 'Verify the proposed deliverable independently and report only material defects or acceptance.',
|
||||
}[phase] || 'Complete the assigned collaboration phase.'
|
||||
const receiptShape = phase === 'proposal'
|
||||
? 'Receipt JSON shape: [[MELDWORK_COLLABORATION:{"summary":"...","capabilities":["..."],"intendedWork":["..."],"deliverables":["..."],"dependencies":["..."]}]]'
|
||||
? 'Receipt JSON shape: [[MELDWORK_COLLABORATION:{"summary":"...","capabilities":["..."],"intendedWork":["..."],"deliverables":["..."],"dependencies":[]}]]'
|
||||
: phase === 'challenge'
|
||||
? 'Receipt JSON shape: either propose a complete graph with [[MELDWORK_COLLABORATION:{"verdict":"support|contradict","summary":"...","proposedAssignments":[{"taskId":"...","ownerKind":"...","role":"worker|integrator|verifier","objective":"...","expectedOutput":"...","inputRefs":[],"artifactIds":[],"dependsOn":[]}],"finalizerKind":"...","verifierKinds":["..."],"agreeToPlan":true}]] or support a listed graph with [[MELDWORK_COLLABORATION:{"verdict":"support","summary":"...","supportedPlanHash":"64 lowercase hex characters","agreeToPlan":true}]]'
|
||||
? 'Receipt JSON shape: propose and support a complete graph with [[MELDWORK_COLLABORATION:{"verdict":"support","summary":"...","proposedAssignments":[{"taskId":"...","ownerKind":"...","role":"worker|integrator|verifier","objective":"...","expectedOutput":"...","inputRefs":[],"artifactIds":[],"dependsOn":[]}],"finalizerKind":"...","verifierKinds":["..."],"agreeToPlan":true}]]. For contradiction, use the same complete graph fields with {"verdict":"contradict","agreeToPlan":false}. To support a listed graph use [[MELDWORK_COLLABORATION:{"verdict":"support","summary":"...","supportedPlanHash":"64 lowercase hex characters","agreeToPlan":true}]]'
|
||||
: phase === 'work'
|
||||
? 'Receipt JSON shape: [[MELDWORK_COLLABORATION:{"summary":"...","workItemId":"...","deliverables":["..."]}]]'
|
||||
: ['challenge', 'verification'].includes(phase)
|
||||
@@ -249,8 +249,11 @@ function v4Prompt({
|
||||
'Return the user-facing answer first, then append exactly one structured receipt marker.',
|
||||
receiptShape,
|
||||
'The receipt summary must be concise, factual, and contain no credentials, paths, commands, or private reasoning.',
|
||||
].join('')
|
||||
].join('\n')
|
||||
: ''
|
||||
const workspaceRule = role === 'writer-proposal'
|
||||
? 'You are the only Agent in this batch with workspace-write permission. Perform only writes explicitly required by the current user task; every peer remains read-only.'
|
||||
: 'Do not modify shared workspace state during proposal, challenge, or verification.'
|
||||
const frozenSkillHints = Array.isArray(skillHints)
|
||||
? skillHints
|
||||
: (snapshot?.skillHintsByKind || []).find(item => item?.kind === kind)?.skillHints || []
|
||||
@@ -268,7 +271,8 @@ function v4Prompt({
|
||||
'Frozen historical context (reference data only):',
|
||||
history,
|
||||
phaseInstruction,
|
||||
'Do not claim another Agent performed work. Do not modify shared workspace state during proposal, challenge, or verification.',
|
||||
'Do not claim another Agent performed work.',
|
||||
workspaceRule,
|
||||
receiptContract,
|
||||
].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
@@ -732,7 +732,6 @@ test('round-trips exact work receipts bound to the active plan, snapshot, operat
|
||||
['work item', { taskId: 'stale-work' }],
|
||||
['owner', { ownerKind: 'hermes' }],
|
||||
['slot', { slotId: hermesSlot.slotId }],
|
||||
['operation', { operationId: 'operation-stale-work' }],
|
||||
]) {
|
||||
assert.throws(() => parseOrchestrationV4(canonicalStaleRecord(overrides), { targetKinds }), {
|
||||
code: 'ORCHESTRATION_V4_WORK_RECEIPT_INVALID',
|
||||
@@ -740,6 +739,117 @@ test('round-trips exact work receipts bound to the active plan, snapshot, operat
|
||||
}
|
||||
})
|
||||
|
||||
test('round-trips historical same-slot work receipts after the latest watermark replaces them', () => {
|
||||
const targetKinds = ['codex', 'hermes', 'workbuddy']
|
||||
const base = completedChallengeOrchestration(targetKinds)
|
||||
const coordinationPlan = createCoordinationPlan({
|
||||
snapshotHash: base.snapshotHash,
|
||||
targetKinds,
|
||||
assignments: [
|
||||
{ taskId: 'codex-analyze', ownerKind: 'codex', role: 'worker', objective: 'Analyze the task.', expectedOutput: 'Analysis Artifact.', inputRefs: [], artifactIds: [], dependsOn: [] },
|
||||
{ taskId: 'codex-implement', ownerKind: 'codex', role: 'worker', objective: 'Implement the task.', expectedOutput: 'Implementation Artifact.', inputRefs: [], artifactIds: [], dependsOn: ['codex-analyze'] },
|
||||
{ taskId: 'hermes-review', ownerKind: 'hermes', role: 'verifier', objective: 'Review the implementation.', expectedOutput: 'Review Artifact.', inputRefs: [], artifactIds: [], dependsOn: ['codex-implement'] },
|
||||
{ taskId: 'integrate-work', ownerKind: 'workbuddy', role: 'integrator', objective: 'Integrate the work.', expectedOutput: 'Integrated Artifact.', inputRefs: [], artifactIds: [], dependsOn: ['hermes-review'] },
|
||||
],
|
||||
finalizerKind: 'workbuddy',
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
agreedBy: targetKinds,
|
||||
})
|
||||
const slot = base.slots.find(candidate => candidate.agentKind === 'codex')
|
||||
const operations = ['operation-codex-analyze', 'operation-codex-implement']
|
||||
const tasks = ['codex-analyze', 'codex-implement']
|
||||
const receipts = operations.map((operationId, index) => {
|
||||
const artifactId = `artifact-${String(index + 1).repeat(64)}`
|
||||
return createCollaborationReceipt({
|
||||
phase: 'work',
|
||||
agentKind: 'codex',
|
||||
slotId: slot.slotId,
|
||||
operationId,
|
||||
status: 'completed',
|
||||
summary: `Codex completed ${tasks[index]}.`,
|
||||
artifactIds: [artifactId],
|
||||
workItemId: tasks[index],
|
||||
snapshotHash: base.snapshotHash,
|
||||
deliveryWatermark: index + 3,
|
||||
})
|
||||
})
|
||||
const workReceipts = receipts.map((receipt, index) => {
|
||||
const contentHash = String(index + 3).repeat(64)
|
||||
return createWorkReceipt({
|
||||
snapshotHash: base.snapshotHash,
|
||||
snapshotBodyHash: base.snapshot.bodyHash,
|
||||
snapshotContentRef: base.snapshot.contentRef,
|
||||
planHash: coordinationPlan.planHash,
|
||||
taskId: tasks[index],
|
||||
ownerKind: 'codex',
|
||||
slotId: slot.slotId,
|
||||
operationId: operations[index],
|
||||
collaborationReceipt: receipt,
|
||||
artifacts: [{
|
||||
artifactId: receipt.artifactIds[0],
|
||||
contentHash,
|
||||
contentRef: contentRef(contentHash, 42),
|
||||
}],
|
||||
})
|
||||
})
|
||||
const latestReceipt = receipts.at(-1)
|
||||
const record = {
|
||||
...base,
|
||||
phase: 'work',
|
||||
coordinationPlan,
|
||||
workReceipts,
|
||||
commitState: { ...base.commitState, writerKind: coordinationPlan.finalizerKind },
|
||||
plan: {
|
||||
...base.plan,
|
||||
assignments: base.plan.assignments.map(assignment => (
|
||||
assignment.agentKind === 'codex'
|
||||
? { ...assignment, operationId: latestReceipt.operationId }
|
||||
: assignment
|
||||
)),
|
||||
},
|
||||
deliveryWatermarks: [{
|
||||
agentKind: 'codex',
|
||||
phase: 'work',
|
||||
watermark: latestReceipt.deliveryWatermark,
|
||||
operationId: latestReceipt.operationId,
|
||||
snapshotHash: base.snapshotHash,
|
||||
updatedAt: 1003,
|
||||
}],
|
||||
slots: base.slots.map(candidate => candidate.slotId === slot.slotId ? {
|
||||
...candidate,
|
||||
phase: 'work',
|
||||
status: 'completed',
|
||||
operationId: latestReceipt.operationId,
|
||||
deliveryWatermark: latestReceipt.deliveryWatermark,
|
||||
receiptId: latestReceipt.receiptId,
|
||||
resultHash: hashValue(latestReceipt),
|
||||
finishedAt: 1003,
|
||||
resultRefs: {
|
||||
...(candidate.resultRefs || {}),
|
||||
artifactIds: workReceipts.flatMap(receipt => (
|
||||
receipt.artifacts.map(artifact => artifact.artifactId)
|
||||
)),
|
||||
workflowOutcomeRefs: [
|
||||
...(candidate.resultRefs?.workflowOutcomeRefs || []),
|
||||
...receipts.map(receipt => ({ receipt })),
|
||||
],
|
||||
},
|
||||
} : candidate),
|
||||
}
|
||||
|
||||
assert.deepEqual(parseOrchestrationV4(record, { targetKinds }), record)
|
||||
|
||||
const tampered = structuredClone(record)
|
||||
const historical = tampered.slots.find(candidate => candidate.slotId === slot.slotId)
|
||||
.resultRefs.workflowOutcomeRefs.find(item => (
|
||||
item.receipt?.receiptId === receipts[0].receiptId
|
||||
))
|
||||
historical.receipt.summary = 'Tampered historical result.'
|
||||
assert.throws(() => parseOrchestrationV4(tampered, { targetKinds }), {
|
||||
code: 'ORCHESTRATION_V4_WORK_RECEIPT_INVALID',
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts a targeted synthesis revision after verification without stale challenge bindings', () => {
|
||||
const targetKinds = ['codex', 'hermes', 'workbuddy']
|
||||
const base = completedChallengeOrchestration(targetKinds)
|
||||
@@ -1949,7 +2059,7 @@ test('requires every Agent to support the same responsibility graph instead of s
|
||||
assert.deepEqual(unanimous.plan.agreedBy, ['codex', 'hermes', 'workbuddy'])
|
||||
})
|
||||
|
||||
test('binds unanimous coordination to each Agent latest support receipt and rejects contradictory agreement', () => {
|
||||
test('binds unanimous coordination to latest support receipts and keeps contradiction unresolved', () => {
|
||||
const targetKinds = ['codex', 'hermes', 'workbuddy']
|
||||
const snapshotHash = 'c'.repeat(64)
|
||||
const assignments = targetKinds.map((ownerKind, index) => ({
|
||||
@@ -2002,7 +2112,7 @@ test('binds unanimous coordination to each Agent latest support receipt and reje
|
||||
|
||||
assert.deepEqual(unanimous.plan.supportReceiptIds, supportReceipts
|
||||
.map(record => record.receipt.receiptId).sort())
|
||||
assert.throws(() => resolveCoordinationConsensus({
|
||||
const contradicted = resolveCoordinationConsensus({
|
||||
targetKinds,
|
||||
snapshotHash,
|
||||
candidateReceipts: [{
|
||||
@@ -2013,7 +2123,11 @@ test('binds unanimous coordination to each Agent latest support receipt and reje
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
}],
|
||||
supportReceipts: [supportRecord('codex', 'contradict'), ...supportReceipts.slice(1)],
|
||||
}), { code: 'ORCHESTRATION_V4_COORDINATION_PLAN_INVALID' })
|
||||
})
|
||||
assert.equal(contradicted.plan, null)
|
||||
assert.deepEqual(contradicted.supportPlanHashes, [
|
||||
'', candidate.planHash, candidate.planHash,
|
||||
])
|
||||
})
|
||||
|
||||
test('ignores an invalid responsibility candidate so the next challenge round can repair it', () => {
|
||||
@@ -2200,6 +2314,36 @@ test('returns the complete index without receipts when Agent prefixes exceed the
|
||||
assert.deepEqual(packageRecord.deliveryWatermarks, [])
|
||||
})
|
||||
|
||||
test('keeps valid receipts valid when the rendered package has only index space', () => {
|
||||
const targetKinds = ['codex', 'hermes']
|
||||
const receipt = createCollaborationReceipt({
|
||||
phase: 'challenge',
|
||||
agentKind: 'hermes',
|
||||
slotId: 'slot-hermes',
|
||||
operationId: 'operation-hermes',
|
||||
status: 'completed',
|
||||
summary: 's'.repeat(700),
|
||||
capabilities: ['c'.repeat(700)],
|
||||
intendedWork: ['i'.repeat(700)],
|
||||
deliverables: ['d'.repeat(700)],
|
||||
snapshotHash: 'a'.repeat(64),
|
||||
deliveryWatermark: 2,
|
||||
})
|
||||
const indexText = [
|
||||
'MELDWORK_V4_COLLABORATION_PACKAGE_V1',
|
||||
'[index] codex',
|
||||
'[index] hermes',
|
||||
].join('\n')
|
||||
|
||||
const packageRecord = buildCollaborationPackage([receipt], {
|
||||
targetKinds,
|
||||
totalLimit: indexText.length,
|
||||
})
|
||||
|
||||
assert.equal(packageRecord.text, indexText)
|
||||
assert.deepEqual(packageRecord.receipts, [])
|
||||
})
|
||||
|
||||
test('reserves every selected Agent latest conclusion before optional package references', () => {
|
||||
const targetKinds = Array.from({ length: 32 }, (_, index) => `agent-${String(index).padStart(2, '0')}`)
|
||||
const receipts = targetKinds.map((agentKind, index) => createCollaborationReceipt({
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const {
|
||||
createCollaborationReceipt,
|
||||
createCoordinationPlan,
|
||||
} = require('../../src/collaboration/orchestration-v4-records.cjs')
|
||||
const { LocalWorkspace } = require('../../src/workspace/local-workspace.cjs')
|
||||
const { RunLedger } = require('../../src/runs/run-ledger.cjs')
|
||||
const { v4Prompt } = require('../../src/workspace/local-workspace-context.cjs')
|
||||
const { deferred, fixture } = require('../support/local-workspace-test-helpers.cjs')
|
||||
|
||||
function proposedAssignments(objectiveSuffix = '') {
|
||||
@@ -54,6 +57,47 @@ function proposedAssignments(objectiveSuffix = '') {
|
||||
]
|
||||
}
|
||||
|
||||
test('V4 proposal receipt shape uses an empty dependencies array without a placeholder', () => {
|
||||
const prompt = v4Prompt({
|
||||
group: {},
|
||||
kind: 'hermes',
|
||||
phase: 'proposal',
|
||||
snapshot: {
|
||||
targetKinds: ['hermes'],
|
||||
group: { name: 'Receipt shape', topic: '' },
|
||||
messageId: 'task-1',
|
||||
taskText: 'Produce an independent proposal.',
|
||||
history: [],
|
||||
skillHintsByKind: [],
|
||||
},
|
||||
})
|
||||
const receiptShape = prompt.split('\n').find(line => line.startsWith('Receipt JSON shape:'))
|
||||
|
||||
assert.match(receiptShape, /"dependencies":\[\]/)
|
||||
assert.doesNotMatch(receiptShape, /"dependencies":\["\.\.\."\]/)
|
||||
})
|
||||
|
||||
test('V4 challenge contract separates finalizer, verifier, and contradiction semantics', () => {
|
||||
const prompt = v4Prompt({
|
||||
group: {},
|
||||
kind: 'hermes',
|
||||
phase: 'challenge',
|
||||
snapshot: {
|
||||
targetKinds: ['codex', 'hermes'],
|
||||
group: { name: 'Coordination contract', topic: '' },
|
||||
messageId: 'task-1',
|
||||
taskText: 'Negotiate responsibilities.',
|
||||
history: [],
|
||||
},
|
||||
})
|
||||
|
||||
assert.match(prompt, /must include one finalizerKind owned by an integrator/)
|
||||
assert.match(prompt, /exactly 1 distinct verifierKinds from the selected Agents/)
|
||||
assert.match(prompt, /verifierKinds must not contain finalizerKind/)
|
||||
assert.match(prompt, /"verdict":"contradict"[\s\S]*"agreeToPlan":false/)
|
||||
assert.doesNotMatch(prompt, /"verdict":"support\|contradict"/)
|
||||
})
|
||||
|
||||
function minimalAssignments(targetKinds) {
|
||||
const finalizerKind = targetKinds.at(-1)
|
||||
return targetKinds.map((ownerKind, index) => ({
|
||||
@@ -950,12 +994,16 @@ test('V4 omits an oversized responsibility candidate rather than exposing a part
|
||||
test('V4 lets Agents negotiate responsibilities, execute every work package, and honor agreed delivery roles', async (t) => {
|
||||
const { directory, options } = fixture()
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
options.runLedger = new RunLedger({
|
||||
storagePath: path.join(directory, 'run-ledger-agent-negotiation.json'),
|
||||
})
|
||||
const calls = []
|
||||
const challengeCounts = new Map()
|
||||
const firstWaveStarted = new Set()
|
||||
const firstWaveCompleted = new Set()
|
||||
const releaseFirstWave = deferred()
|
||||
let preferredPlan = null
|
||||
let invalidPlan = null
|
||||
let workspace = null
|
||||
let group = null
|
||||
let frozenInput = null
|
||||
@@ -1019,6 +1067,22 @@ test('V4 lets Agents negotiate responsibilities, execute every work package, and
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
agreedBy: ['codex', 'hermes', 'workbuddy'],
|
||||
})
|
||||
invalidPlan ||= createCoordinationPlan({
|
||||
snapshotHash,
|
||||
targetKinds: ['codex', 'hermes', 'workbuddy'],
|
||||
assignments: referencedAssignments().map((assignment) => (
|
||||
assignment.taskId === 'assemble-team-delivery'
|
||||
? {
|
||||
...assignment,
|
||||
inputRefs: ['acceptance.txt'],
|
||||
artifactIds: ['future-acceptance-artifact'],
|
||||
}
|
||||
: assignment
|
||||
)),
|
||||
finalizerKind: 'workbuddy',
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
agreedBy: ['codex', 'hermes', 'workbuddy'],
|
||||
})
|
||||
if (count === 1) {
|
||||
const reviewedKind = prompt.match(/^Proposal Agent: ([A-Za-z0-9._:-]+)$/m)?.[1] || ''
|
||||
const reviewedArtifactId = prompt.match(/^Proposal Artifact: (artifact-[a-f0-9]{64})$/m)?.[1] || ''
|
||||
@@ -1029,9 +1093,9 @@ test('V4 lets Agents negotiate responsibilities, execute every work package, and
|
||||
assert.match(prompt, /Useful public proposal detail\./)
|
||||
assert.match(prompt, /exactly 2 distinct verifierKinds/)
|
||||
assert.doesNotMatch(prompt, /session-secret|MELDWORK_PRIVATE|hidden reasoning|private_reasoning/i)
|
||||
const assignments = agent.kind === 'hermes'
|
||||
? proposedAssignments(' using an alternative scope')
|
||||
: preferredPlan.assignments
|
||||
assert.match(prompt, /inputRefs are exact displayed frozen Source IDs/)
|
||||
assert.match(prompt, /artifactIds are existing immutable Artifact IDs/)
|
||||
assert.match(prompt, /future outputs flow only through dependsOn/)
|
||||
return {
|
||||
text: `${agent.kind} responsibility proposal`,
|
||||
sessionRef: runOptions.sessionRef || `${agent.kind}-session`,
|
||||
@@ -1040,41 +1104,33 @@ test('V4 lets Agents negotiate responsibilities, execute every work package, and
|
||||
phase,
|
||||
verdict: 'support',
|
||||
summary: `${agent.kind} proposes a responsibility graph`,
|
||||
proposedAssignments: assignments,
|
||||
proposedAssignments: invalidPlan.assignments,
|
||||
finalizerKind: 'workbuddy',
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
agreeToPlan: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
assert.match(prompt, new RegExp(preferredPlan.planHash))
|
||||
assert.match(prompt, /Fields: h=planHash/)
|
||||
assert.match(prompt, /"f":"workbuddy"/)
|
||||
assert.match(prompt, /"v":\["codex","hermes"\]/)
|
||||
for (const assignment of preferredPlan.assignments) {
|
||||
assert.ok(prompt.includes(`"t":"${assignment.taskId}"`))
|
||||
assert.ok(prompt.includes(`"o":"${assignment.ownerKind}"`))
|
||||
assert.ok(prompt.includes(`"r":"${assignment.role}"`))
|
||||
assert.ok(prompt.includes(`"x":"${assignment.objective}"`))
|
||||
assert.ok(prompt.includes(`"y":"${assignment.expectedOutput}"`))
|
||||
assert.ok(prompt.includes(`"i":${JSON.stringify(assignment.inputRefs)}`))
|
||||
assert.ok(prompt.includes(`"a":${JSON.stringify(assignment.artifactIds)}`))
|
||||
assert.ok(prompt.includes(`"d":${JSON.stringify(assignment.dependsOn)}`))
|
||||
}
|
||||
assert.doesNotMatch(prompt, new RegExp(invalidPlan.planHash))
|
||||
return {
|
||||
text: `${agent.kind} supports the shared responsibility graph`,
|
||||
text: `${agent.kind} supports a corrected responsibility graph`,
|
||||
sessionRef: runOptions.sessionRef || `${agent.kind}-session`,
|
||||
collaboration: {
|
||||
version: 1,
|
||||
phase,
|
||||
verdict: 'support',
|
||||
summary: `${agent.kind} supports the same responsibility graph`,
|
||||
supportedPlanHash: preferredPlan.planHash,
|
||||
summary: `${agent.kind} supports the corrected responsibility graph`,
|
||||
proposedAssignments: preferredPlan.assignments,
|
||||
finalizerKind: 'workbuddy',
|
||||
verifierKinds: ['codex', 'hermes'],
|
||||
agreeToPlan: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
if (phase === 'work') {
|
||||
assert.deepEqual([...challengeCounts.entries()].sort(), [
|
||||
['codex', 2], ['hermes', 2], ['workbuddy', 2],
|
||||
])
|
||||
const workItemId = prompt.match(/^Work item: ([A-Za-z0-9._:-]+)$/m)?.[1] || ''
|
||||
assert.ok(workItemId)
|
||||
if (['inspect-current-behavior', 'implement-bounded-change'].includes(workItemId)) {
|
||||
@@ -1245,6 +1301,9 @@ test('V4 lets Agents negotiate responsibilities, execute every work package, and
|
||||
content: message.content,
|
||||
})),
|
||||
}, null, 2))
|
||||
assert.deepEqual([...challengeCounts.entries()].sort(), [
|
||||
['codex', 2], ['hermes', 2], ['workbuddy', 2],
|
||||
])
|
||||
assert.ok(integratorContext)
|
||||
const persistedPlan = controller.orchestration.coordinationPlan
|
||||
const { supportReceiptIds, ...persistedGraph } = persistedPlan
|
||||
|
||||
@@ -352,6 +352,65 @@ test('Auto V4 recovers an accepted candidate over 6000 characters without trunca
|
||||
})
|
||||
})
|
||||
|
||||
test('fresh Auto observes terminal persistence failure without hiding controller promise rejection', async (t) => {
|
||||
const { directory, options } = fixture()
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
|
||||
const ledger = new RunLedger({ storagePath: path.join(directory, 'run-ledger-terminal-failure.json') })
|
||||
options.runLedger = ledger
|
||||
options.terminalRetrySleep = () => Promise.resolve()
|
||||
const workspace = new LocalWorkspace(options)
|
||||
await workspace.refreshAgents()
|
||||
const group = workspace.createGroup({
|
||||
name: 'Fresh Auto terminal persistence failure',
|
||||
agentKinds: ['codex', 'hermes'],
|
||||
workdir: directory,
|
||||
allowWrite: false,
|
||||
})
|
||||
workspace.autoRunner.runV4Discussion = async () => 'completed'
|
||||
ledger.finish = () => { throw new Error('TEST_TERMINAL_PERSISTENCE_UNAVAILABLE') }
|
||||
const unhandled = []
|
||||
const onUnhandled = reason => { unhandled.push(reason) }
|
||||
process.on('unhandledRejection', onUnhandled)
|
||||
t.after(() => process.removeListener('unhandledRejection', onUnhandled))
|
||||
|
||||
await workspace.sendMessage({
|
||||
groupId: group.id,
|
||||
text: 'Finish this fresh Auto run with a durable terminal checkpoint.',
|
||||
mode: 'auto',
|
||||
maxRounds: 2,
|
||||
targetKinds: ['codex', 'hermes'],
|
||||
protocol: 'v4',
|
||||
})
|
||||
const controller = workspace.activeRuns.get(group.id)
|
||||
assert.ok(controller)
|
||||
t.after(() => {
|
||||
for (const timer of workspace.runCheckpointTimers.values()) clearTimeout(timer)
|
||||
workspace.runCheckpointTimers.clear()
|
||||
workspace.activeRuns.delete(group.id)
|
||||
controller.resolveDone()
|
||||
})
|
||||
const originalPromise = controller.promise
|
||||
await waitFor(() => controller.terminalPersistence?.state === 'failed', 'failed terminal outbox')
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
|
||||
assert.deepEqual(unhandled, [])
|
||||
assert.equal(controller.promise, originalPromise)
|
||||
assert.equal(workspace.activeRuns.get(group.id), controller)
|
||||
assert.equal(controller.finished, false)
|
||||
assert.equal(controller.terminalOutbox.status, 'completed')
|
||||
assert.deepEqual(controller.terminalPersistence, {
|
||||
state: 'failed',
|
||||
status: 'completed',
|
||||
attempts: 3,
|
||||
nextRetryAt: 0,
|
||||
code: 'LOCAL_RUN_PERSIST_FAILED',
|
||||
})
|
||||
await assert.rejects(originalPromise, error => (
|
||||
error?.code === 'LOCAL_RUN_TERMINAL_PERSIST_FAILED'
|
||||
&& error.persistence?.state === 'failed'
|
||||
))
|
||||
})
|
||||
|
||||
function negotiatedAssignments() {
|
||||
return [
|
||||
{
|
||||
@@ -487,7 +546,7 @@ test('Auto V4 coordination recovery uses the byte-identical frozen snapshot afte
|
||||
const record = recovery.recoveryLedger.get(input.crashRecord.runId)
|
||||
return ['completed', 'failed', 'partial', 'stopped', 'interrupted'].includes(record?.status)
|
||||
? record : null
|
||||
}, 'coordination recovery', 5000)
|
||||
}, 'coordination recovery', 15000)
|
||||
|
||||
assert.equal(final.status, 'completed', JSON.stringify({
|
||||
reason: final.reason,
|
||||
@@ -600,7 +659,7 @@ test('Auto V4 first dispatch and restart prompts use frozen target-scoped Skill
|
||||
const record = recovery.recoveryLedger.get(input.crashRecord.runId)
|
||||
return ['completed', 'failed', 'partial', 'stopped', 'interrupted'].includes(record?.status)
|
||||
? record : null
|
||||
}, 'challenge recovery with mutable live attachment', 5000)
|
||||
}, 'challenge recovery with mutable live attachment', 15000)
|
||||
|
||||
assert.equal(final.status, 'completed', final.reason)
|
||||
assert.equal(attachmentResolutionCalls, 0)
|
||||
@@ -669,6 +728,7 @@ test('Auto V4 checkpoints each concurrent work receipt before the dependency wav
|
||||
const ledger = new RunLedger({ storagePath: path.join(directory, 'run-ledger-wave.json') })
|
||||
options.runLedger = ledger
|
||||
const delayedHermes = deferred()
|
||||
t.after(() => delayedHermes.resolve())
|
||||
const assignments = [
|
||||
{
|
||||
taskId: 'codex-work', ownerKind: 'codex', role: 'worker',
|
||||
@@ -747,7 +807,7 @@ test('Auto V4 checkpoints each concurrent work receipt before the dependency wav
|
||||
targetKinds: ['codex', 'hermes', 'workbuddy'], protocol: 'v4',
|
||||
})
|
||||
const controller = workspace.activeRuns.get(group.id)
|
||||
await waitFor(() => crashRecord, 'first work receipt checkpoint')
|
||||
await waitFor(() => crashRecord, 'first work receipt checkpoint', 15000)
|
||||
delayedHermes.resolve()
|
||||
await controller.promise
|
||||
|
||||
@@ -773,7 +833,7 @@ test('Auto V4 checkpoints each concurrent work receipt before the dependency wav
|
||||
const record = recovery.recoveryLedger.get(crashRecord.runId)
|
||||
return ['completed', 'failed', 'partial', 'stopped', 'interrupted'].includes(record?.status)
|
||||
? record : null
|
||||
}, 'exact work receipt recovery')
|
||||
}, 'exact work receipt recovery', 15000)
|
||||
const recoveredWorkKinds = recovery.calls
|
||||
.filter(call => /^Phase: work$/m.test(call.prompt))
|
||||
.map(call => call.agent.kind)
|
||||
|
||||
@@ -450,11 +450,12 @@ test('Manual V4 recovery safely invokes a queued writer that never acquired a le
|
||||
storagePath: recoveryStoragePath,
|
||||
runLedger: recoveryLedger,
|
||||
runScheduler: new RunScheduler({ taskLimit: 1, workspaceLimit: 1, globalLimit: 1 }),
|
||||
runAgent: async (agent, _prompt, _workdir, runOptions) => {
|
||||
runAgent: async (agent, prompt, _workdir, runOptions) => {
|
||||
recoveryCalls.push({
|
||||
kind: agent.kind,
|
||||
operationId: runOptions.operationId,
|
||||
sandbox: runOptions.sandbox,
|
||||
prompt,
|
||||
})
|
||||
return {
|
||||
text: `${agent.kind} recovered queued proposal`,
|
||||
@@ -485,6 +486,13 @@ test('Manual V4 recovery safely invokes a queued writer that never acquired a le
|
||||
const slot = crashRecord.orchestration.slots.find(item => item.agentKind === call.kind)
|
||||
assert.equal(call.operationId, slot.operationId)
|
||||
assert.equal(call.sandbox, call.kind === writerKind ? 'workspace-write' : 'read-only')
|
||||
assert.match(call.prompt, /structured receipt marker\.\nReceipt JSON shape:/)
|
||||
if (call.kind === writerKind) {
|
||||
assert.match(call.prompt, /only Agent in this batch with workspace-write permission/)
|
||||
assert.doesNotMatch(call.prompt, /Do not modify shared workspace state during proposal/)
|
||||
} else {
|
||||
assert.match(call.prompt, /Do not modify shared workspace state during proposal/)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(final.orchestration.commitState.committedKinds, [
|
||||
'codex', 'workbuddy', 'hermes',
|
||||
|
||||
@@ -363,12 +363,12 @@ describe('Meldwork workbench', () => {
|
||||
await flushPromises()
|
||||
expect(writeText).not.toHaveBeenCalled()
|
||||
|
||||
expect(wrapper.find('.message-row.agent .message-meta-actions .message-copy-button').exists()).toBe(false)
|
||||
await wrapper.get('.message-row.agent .message-footer-actions .message-copy-button').trigger('click')
|
||||
expect(wrapper.find('.message-row.agent .message-footer-actions .message-copy-button').exists()).toBe(false)
|
||||
await wrapper.get('.message-row.agent .message-meta-actions .message-copy-button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(writeText).toHaveBeenCalledWith('[Open docs](https://example.com)\n\nCopy this answer.')
|
||||
expect(wrapper.get('.message-row.agent').classes()).toContain('copied')
|
||||
expect(wrapper.get('.message-row.agent .message-footer-actions .message-copy-button').attributes('aria-label')).toBe('Copied')
|
||||
expect(wrapper.get('.message-row.agent .message-meta-actions .message-copy-button').attributes('aria-label')).toBe('Copied')
|
||||
expect(wrapper.get('.copy-toast-message').text()).toBe('Copied to clipboard')
|
||||
|
||||
writeText.mockClear()
|
||||
@@ -379,7 +379,7 @@ describe('Meldwork workbench', () => {
|
||||
const execCommand = vi.fn(() => true)
|
||||
Object.defineProperty(document, 'execCommand', { configurable: true, value: execCommand })
|
||||
writeText.mockRejectedValueOnce(new Error('Clipboard permission denied'))
|
||||
await wrapper.get('.message-row.agent .message-footer-actions .message-copy-button').trigger('click')
|
||||
await wrapper.get('.message-row.agent .message-meta-actions .message-copy-button').trigger('click')
|
||||
await flushPromises()
|
||||
expect(execCommand).toHaveBeenCalledWith('copy')
|
||||
expect(wrapper.find('.toast-message').exists()).toBe(false)
|
||||
@@ -866,6 +866,7 @@ describe('Meldwork workbench', () => {
|
||||
it('keeps direct Human Gates inline and sends exact approve or reject decisions', async () => {
|
||||
const allowGateId = `human-gate-${'a'.repeat(64)}`
|
||||
const rejectGateId = `human-gate-${'b'.repeat(64)}`
|
||||
const recoveryGateId = `human-gate-${'c'.repeat(64)}`
|
||||
const gate = (gateId, type = 'permission') => ({
|
||||
gateId,
|
||||
type,
|
||||
@@ -909,7 +910,7 @@ describe('Meldwork workbench', () => {
|
||||
threadRootId: 'direct-gated-root',
|
||||
targetKinds: ['codex'],
|
||||
currentKind: 'codex',
|
||||
waitingGateIds: [allowGateId, rejectGateId],
|
||||
waitingGateIds: [allowGateId, rejectGateId, recoveryGateId],
|
||||
budget: {
|
||||
limits: {
|
||||
inputTokens: 4000, outputTokens: null, costMicros: null,
|
||||
@@ -942,14 +943,25 @@ describe('Meldwork workbench', () => {
|
||||
events: [],
|
||||
}],
|
||||
}]
|
||||
state.humanGates = [gate(allowGateId), gate(rejectGateId, 'budget')]
|
||||
state.humanGates = [
|
||||
gate(allowGateId),
|
||||
gate(rejectGateId, 'budget'),
|
||||
{
|
||||
...gate(recoveryGateId, 'decision'),
|
||||
summary: 'The candidate and unresolved issues have remained unchanged for two rounds.',
|
||||
options: [
|
||||
{ optionId: 'retry-original-writer', name: 'Accept', kind: 'accept' },
|
||||
{ optionId: 'replace-next-writer', name: 'Accept', kind: 'accept' },
|
||||
],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
await wrapper.get('.direct-session-open').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.run-trace-panel').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.direct-human-gate-list .human-gate-card')).toHaveLength(2)
|
||||
expect(wrapper.findAll('.direct-human-gate-list .human-gate-card')).toHaveLength(3)
|
||||
expect(wrapper.get('.direct-human-gate-list').text()).toContain('This Agent needs permission')
|
||||
expect(wrapper.get('.direct-human-gate-list').text()).toContain('cannot report cost usage')
|
||||
expect(wrapper.get('.direct-budget-details').text()).toContain('Input tokens')
|
||||
@@ -957,6 +969,12 @@ describe('Meldwork workbench', () => {
|
||||
expect(wrapper.get('.direct-budget-details').text()).toContain('Hard budget stop')
|
||||
|
||||
const gateCards = wrapper.findAll('.direct-human-gate-list .human-gate-card')
|
||||
const recoveryButtons = gateCards[2].findAll('button')
|
||||
expect(recoveryButtons.map(button => button.text())).toEqual([
|
||||
'Retry original writer',
|
||||
'Use next writer',
|
||||
])
|
||||
expect(recoveryButtons[0].text()).not.toBe(recoveryButtons[1].text())
|
||||
await gateCards[0].findAll('button').find(button => button.text() === 'Allow once').trigger('click')
|
||||
await gateCards[1].findAll('button').find(button => button.text() === 'Reject').trigger('click')
|
||||
await flushPromises()
|
||||
@@ -971,6 +989,9 @@ describe('Meldwork workbench', () => {
|
||||
rejectGateId,
|
||||
{ optionId: 'reject-once' },
|
||||
)
|
||||
setLocale('zh')
|
||||
await flushPromises()
|
||||
expect(wrapper.get('.direct-human-gate-list').text()).toContain('候选结论与未解决问题已连续两轮没有变化')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -1820,14 +1841,17 @@ describe('Meldwork workbench', () => {
|
||||
expect(codexReply().text()).toContain('Codex regenerated response')
|
||||
expect(codexReply().get('.response-version-controls').text()).toContain('2/2')
|
||||
expect(codexReply().findAll('.message-meta-actions > button').map(button => button.classes()[0]))
|
||||
.toEqual(['message-reply-toggle'])
|
||||
.toEqual(['message-copy-button', 'message-reply-toggle'])
|
||||
expect(codexReply().findAll('.message-footer-actions > button').map(button => button.classes()[0]))
|
||||
.toEqual(['message-regenerate-button', 'message-copy-button', 'message-delete-button'])
|
||||
.toEqual(['message-regenerate-button', 'message-delete-button'])
|
||||
expect(codexReply().get('.message-footer-actions .response-version-controls').text()).toContain('2/2')
|
||||
expect(codexReply().find('.message-footer-actions .message-copy-button').exists()).toBe(true)
|
||||
expect(codexReply().find('.message-footer-actions .message-copy-button').exists()).toBe(false)
|
||||
expect(codexReply().find('.message-meta-actions .message-copy-button').exists()).toBe(true)
|
||||
expect(hermesReply().find('.response-version-controls').exists()).toBe(false)
|
||||
expect(hermesReply().findAll('.message-meta-actions > button').map(button => button.classes()[0]))
|
||||
.toEqual(['message-copy-button', 'message-reply-toggle'])
|
||||
expect(hermesReply().findAll('.message-footer-actions > button').map(button => button.classes()[0]))
|
||||
.toEqual(['message-regenerate-button', 'message-copy-button', 'message-delete-button'])
|
||||
.toEqual(['message-regenerate-button', 'message-delete-button'])
|
||||
|
||||
await codexReply().get('.message-reply-toggle').trigger('click')
|
||||
expect(codexReply().classes()).toContain('agent-reply-collapsed')
|
||||
@@ -1841,7 +1865,7 @@ describe('Meldwork workbench', () => {
|
||||
expect(styles).toMatch(/\.message-footer-actions \.response-version-controls\s*\{[^}]*margin-right:\s*auto;/s)
|
||||
expect(styles).toMatch(/\.message-row\.agent-reply-collapsed \.message-body\s*\{[^}]*padding-bottom:\s*34px;/s)
|
||||
expect(styles).toMatch(/\.message-row\.agent-reply-collapsed \.message-footer-actions\s*\{[^}]*display:\s*inline-flex;/s)
|
||||
expect(styles).toMatch(/\.message-row\.agent-reply-collapsed \.message-footer-actions > \.message-regenerate-button,[^}]+\.message-row\.agent-reply-collapsed \.message-footer-actions > \.message-copy-button,[^}]+\.message-row\.agent-reply-collapsed \.message-footer-actions > \.message-delete-button\s*\{[^}]*display:\s*none;/s)
|
||||
expect(styles).toMatch(/\.message-row\.agent-reply-collapsed \.message-footer-actions > \.message-regenerate-button,[^}]+\.message-row\.agent-reply-collapsed \.message-footer-actions > \.message-delete-button\s*\{[^}]*display:\s*none;/s)
|
||||
expect(styles).toMatch(/\.response-version-controls button:hover::after,\s*\.response-version-controls button:focus-visible::after\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translate\(-50%, 0\);/s)
|
||||
expect(styles).toMatch(/\.response-version-controls button:last-child::after\s*\{[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*transform:\s*translateY\(-2px\);/s)
|
||||
expect(styles).toMatch(/\.response-version-controls button:first-child:hover::after,[^}]+\.response-version-controls button:last-child:focus-visible::after\s*\{[^}]*transform:\s*translateY\(0\);/s)
|
||||
@@ -1852,8 +1876,8 @@ describe('Meldwork workbench', () => {
|
||||
expect(codexReply().get('.message-footer-actions .response-version-controls').isVisible()).toBe(true)
|
||||
expect(codexReply().findAll('.message-footer-actions .response-version-controls button')).toHaveLength(2)
|
||||
expect(codexReply().find('.message-footer-actions .message-regenerate-button').isVisible()).toBe(false)
|
||||
expect(codexReply().find('.message-footer-actions .message-copy-button').isVisible()).toBe(false)
|
||||
expect(codexReply().find('.message-footer-actions .message-delete-button').isVisible()).toBe(false)
|
||||
expect(codexReply().find('.message-meta-actions .message-copy-button').isVisible()).toBe(true)
|
||||
|
||||
await codexReply().get('.response-version-controls button').trigger('click')
|
||||
expect(codexReply().classes()).toContain('agent-reply-collapsed')
|
||||
|
||||
@@ -287,11 +287,12 @@ describe('Meldwork workbench', () => {
|
||||
runId: 'run-runtime-controls',
|
||||
agentRunId: 'agent-runtime-codex',
|
||||
agentKind: 'codex',
|
||||
summary: 'This run requires a human decision.',
|
||||
summary: 'The workspace-write synthesis attempt may have produced side effects, but its result is unknown.',
|
||||
options: [
|
||||
{ optionId: 'accept-artifact', name: 'Accept Artifact', kind: 'accept' },
|
||||
{ optionId: 'reject-artifact', name: 'Reject Artifact', kind: 'reject' },
|
||||
{ optionId: 'reopen-task', name: 'Reopen Task', kind: 'reopen' },
|
||||
{ optionId: 'continue-discussion', name: 'Accept', kind: 'accept' },
|
||||
{ optionId: 'stop-discussion', name: 'Reject', kind: 'reject' },
|
||||
{ optionId: 'retry-original-writer', name: 'Accept', kind: 'accept' },
|
||||
{ optionId: 'replace-next-writer', name: 'Accept', kind: 'accept' },
|
||||
],
|
||||
status: 'pending',
|
||||
createdAt: '2026-08-04T08:00:00.000Z',
|
||||
@@ -304,22 +305,29 @@ describe('Meldwork workbench', () => {
|
||||
|
||||
expect(wrapper.get('.trace-waiting-state').text()).toBe('Waiting for your decision')
|
||||
expect(wrapper.get('.trace-human-gate-section').text())
|
||||
.toContain('This run requires your decision')
|
||||
.toContain('workspace-write synthesis attempt may have produced side effects')
|
||||
expect(wrapper.find('.trace-budget-section').exists()).toBe(false)
|
||||
expect(wrapper.find('.trace-agent-control-section').exists()).toBe(false)
|
||||
const gateButtons = wrapper.findAll('.trace-human-gate-options button')
|
||||
expect(gateButtons.find(button => button.text() === 'Accept Artifact').classes())
|
||||
expect(gateButtons.find(button => button.text() === 'Retry original writer').classes())
|
||||
.toContain('primary-button')
|
||||
expect(gateButtons.find(button => button.text() === 'Reopen Task').classes())
|
||||
expect(gateButtons.find(button => button.text() === 'Use next writer').classes())
|
||||
.toContain('primary-button')
|
||||
expect(gateButtons.find(button => button.text() === 'Retry original writer').text())
|
||||
.not.toBe(gateButtons.find(button => button.text() === 'Use next writer').text())
|
||||
expect(gateButtons.find(button => button.text() === 'Stop discussion').classes())
|
||||
.toContain('secondary-button')
|
||||
await gateButtons.find(button => button.text() === 'Reopen Task').trigger('click')
|
||||
await gateButtons.find(button => button.text() === 'Use next writer').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(bridge.localWorkspace.controlAgent).not.toHaveBeenCalled()
|
||||
expect(bridge.localWorkspace.decideHumanGate).toHaveBeenCalledWith(
|
||||
gateId,
|
||||
{ optionId: 'reopen-task' },
|
||||
{ optionId: 'replace-next-writer' },
|
||||
)
|
||||
setLocale('zh')
|
||||
await flushPromises()
|
||||
expect(wrapper.get('.trace-human-gate-section').text()).toContain('工作区写入综合尝试可能已产生副作用,但结果未知')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -843,6 +843,7 @@ describe('Meldwork workbench', () => {
|
||||
|
||||
expect(mobileRules).toMatch(/\.sidebar\.collapsed\s*\{[^}]*min-height:\s*0;/)
|
||||
expect(mobileRules).toMatch(/\.sidebar\.collapsed\s*>\s*:not\(\.brand-row\)\s*\{[^}]*display:\s*none;/)
|
||||
expect(mobileRules).toMatch(/\.app-shell,\s*\.app-shell\.sidebar-collapsed\s*\{[^}]*overflow-x:\s*hidden;[^}]*overflow-y:\s*auto;/s)
|
||||
})
|
||||
|
||||
it('lets the conversation and composer grow with wide desktop windows', () => {
|
||||
|
||||
@@ -10,13 +10,28 @@ describe('Conversation viewport', () => {
|
||||
liveOutputSignature: ref(''),
|
||||
selectedGroupId,
|
||||
})
|
||||
const scroller = { scrollHeight: 1200, scrollTop: 280, clientHeight: 400 }
|
||||
const container = document.createElement('section')
|
||||
container.className = 'conversation-pane'
|
||||
const scroller = document.createElement('div')
|
||||
scroller.className = 'message-scroll'
|
||||
Object.defineProperties(scroller, {
|
||||
scrollHeight: { value: 1200 },
|
||||
clientHeight: { value: 400 },
|
||||
})
|
||||
scroller.scrollTop = 280
|
||||
const outerReveals = []
|
||||
container.scrollIntoView = options => outerReveals.push({ options, scrollTop: scroller.scrollTop })
|
||||
container.append(scroller)
|
||||
viewport.messageScroller.value = scroller
|
||||
|
||||
selectedGroupId.value = 'group-two'
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(outerReveals).toEqual([{
|
||||
options: { block: 'end', inline: 'nearest' },
|
||||
scrollTop: 280,
|
||||
}])
|
||||
expect(scroller.scrollTop).toBe(1200)
|
||||
})
|
||||
|
||||
@@ -26,7 +41,18 @@ describe('Conversation viewport', () => {
|
||||
liveOutputSignature: ref(''),
|
||||
selectedGroupId: ref('group-one'),
|
||||
})
|
||||
const scroller = { scrollHeight: 1200, scrollTop: 400, clientHeight: 400 }
|
||||
const container = document.createElement('section')
|
||||
container.className = 'workspace-pane'
|
||||
const scroller = document.createElement('div')
|
||||
scroller.className = 'message-scroll'
|
||||
Object.defineProperties(scroller, {
|
||||
scrollHeight: { value: 1200 },
|
||||
clientHeight: { value: 400 },
|
||||
})
|
||||
scroller.scrollTop = 400
|
||||
const outerReveals = []
|
||||
container.scrollIntoView = options => outerReveals.push(options)
|
||||
container.append(scroller)
|
||||
viewport.messageScroller.value = scroller
|
||||
|
||||
viewport.handleMessageScroll()
|
||||
@@ -34,6 +60,7 @@ describe('Conversation viewport', () => {
|
||||
|
||||
await viewport.scrollToLatest({ force: true })
|
||||
|
||||
expect(outerReveals).toEqual([{ block: 'end', inline: 'nearest' }])
|
||||
expect(scroller.scrollTop).toBe(1200)
|
||||
expect(viewport.showScrollToLatest.value).toBe(false)
|
||||
})
|
||||
|
||||
@@ -468,6 +468,17 @@ describe('run event normalization', () => {
|
||||
.toEqual({ version: 4, phase: 'committed', currentKinds: ['codex'] })
|
||||
})
|
||||
|
||||
it('retains every supported V4 orchestration phase for renderer feedback', () => {
|
||||
for (const phase of [
|
||||
'prepare', 'dispatch', 'running', 'reconcile', 'proposal', 'challenge', 'coordination',
|
||||
'work', 'synthesis', 'verification', 'commit', 'committed', 'failed', 'stopped', 'human-gate',
|
||||
]) {
|
||||
expect(normalizeOrchestration({ version: 4, phase })).toEqual({ version: 4, phase })
|
||||
}
|
||||
expect(normalizeOrchestration({ version: 4, phase: 'completed' }))
|
||||
.toEqual({ version: 4, phase: 'committed' })
|
||||
})
|
||||
|
||||
it('keeps a version-only V4 run shell strict under hostile renderer input', () => {
|
||||
const snapshot = normalizeSnapshot({
|
||||
agents: [],
|
||||
|
||||
@@ -47,13 +47,25 @@ describe('Meldwork i18n', () => {
|
||||
expect(t('humanGate.option.allowOnce')).toBe('Allow once')
|
||||
expect(t('humanGate.summary.decision')).toBe('This run requires your decision.')
|
||||
expect(t('humanGate.summary.retry')).toContain('may already have changed')
|
||||
expect(t('humanGate.summary.stalledCandidate')).toContain('unchanged for two rounds')
|
||||
expect(t('humanGate.summary.unknownWriteSynthesis')).toContain('result is unknown')
|
||||
expect(t('humanGate.option.retryOnce')).toBe('Retry once')
|
||||
expect(t('humanGate.option.continueDiscussion')).toBe('Continue discussion')
|
||||
expect(t('humanGate.option.stopDiscussion')).toBe('Stop discussion')
|
||||
expect(t('humanGate.option.retryOriginalWriter')).toBe('Retry original writer')
|
||||
expect(t('humanGate.option.replaceNextWriter')).toBe('Use next writer')
|
||||
expect(t('humanGate.option.acceptArtifact')).toBe('Accept Artifact')
|
||||
expect(t('humanGate.type.input')).toBe('Input')
|
||||
expect(t('humanGate.inputPlaceholder')).toBe('Enter your response')
|
||||
expect(t('trace.retryAgent')).toBe('Retry Agent')
|
||||
expect(t('trace.budgetDimension.toolCalls')).toBe('Tool calls')
|
||||
expect(t('trace.budgetExhaustion')).toBe('Hard budget stop')
|
||||
expect(t('run.phase.prepare')).toBe('Preparing orchestration')
|
||||
expect(t('run.phase.dispatch')).toBe('Dispatching Agents')
|
||||
expect(t('run.phase.running')).toBe('Running')
|
||||
expect(t('run.phase.reconcile')).toBe('Reconciling results')
|
||||
expect(t('run.phase.commit')).toBe('Committing delivery')
|
||||
expect(t('run.phase.failed')).toBe('Failed')
|
||||
expect(t('run.status.circuitBreaker')).toBe('Circuit breaker stopped')
|
||||
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('hard run budget')
|
||||
|
||||
@@ -62,13 +74,25 @@ describe('Meldwork i18n', () => {
|
||||
expect(t('humanGate.option.allowOnce')).toBe('允许一次')
|
||||
expect(t('humanGate.summary.decision')).toBe('该运行需要你做出决定。')
|
||||
expect(t('humanGate.summary.retry')).toContain('可能已经修改')
|
||||
expect(t('humanGate.summary.stalledCandidate')).toContain('连续两轮没有变化')
|
||||
expect(t('humanGate.summary.unknownWriteSynthesis')).toContain('结果未知')
|
||||
expect(t('humanGate.option.retryOnce')).toBe('重试一次')
|
||||
expect(t('humanGate.option.continueDiscussion')).toBe('继续讨论')
|
||||
expect(t('humanGate.option.stopDiscussion')).toBe('停止讨论')
|
||||
expect(t('humanGate.option.retryOriginalWriter')).toBe('重试原写入者')
|
||||
expect(t('humanGate.option.replaceNextWriter')).toBe('改由下一位写入者执行')
|
||||
expect(t('humanGate.option.acceptArtifact')).toBe('接受产物')
|
||||
expect(t('humanGate.type.input')).toBe('补充输入')
|
||||
expect(t('humanGate.inputPlaceholder')).toBe('请输入回复内容')
|
||||
expect(t('trace.retryAgent')).toBe('重试 Agent')
|
||||
expect(t('trace.budgetDimension.toolCalls')).toBe('工具调用')
|
||||
expect(t('trace.budgetExhaustion')).toBe('硬预算停止')
|
||||
expect(t('run.phase.commit')).toBe('提交交付结果')
|
||||
expect(t('run.phase.reconcile')).toBe('汇总结果')
|
||||
expect(t('run.phase.prepare')).toBe('准备编排')
|
||||
expect(t('run.phase.dispatch')).toBe('分发 Agent')
|
||||
expect(t('run.phase.running')).toBe('运行中')
|
||||
expect(t('run.phase.failed')).toBe('失败')
|
||||
expect(t('run.status.circuitBreaker')).toBe('已触发熔断器')
|
||||
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('硬预算上限')
|
||||
})
|
||||
|
||||
@@ -188,6 +188,19 @@
|
||||
<CheckmarkCircleOutline v-if="messageDeleteArmedId === message.id" />
|
||||
<TrashOutline v-else />
|
||||
</button>
|
||||
<button
|
||||
v-if="message.role === 'agent' && message.content"
|
||||
class="message-copy-button"
|
||||
type="button"
|
||||
:data-tooltip="isMessageCopied(message.id) ? t('conversation.copied') : t('conversation.copyMessage')"
|
||||
:aria-label="isMessageCopied(message.id) ? t('conversation.copied') : t('conversation.copyMessage')"
|
||||
@click.stop="copyMessageContent(message, $event, true)"
|
||||
@keydown.enter.prevent="copyMessageContent(message, $event, true)"
|
||||
@keydown.space.prevent="copyMessageContent(message, $event, true)"
|
||||
>
|
||||
<CheckmarkCircleOutline v-if="isMessageCopied(message.id)" />
|
||||
<CopyOutline v-else />
|
||||
</button>
|
||||
<button
|
||||
v-if="message.role === 'agent'"
|
||||
class="message-reply-toggle"
|
||||
@@ -477,19 +490,6 @@
|
||||
>
|
||||
<RefreshOutline :class="{ spinning: isMessageRegenerating(message) }" />
|
||||
</button>
|
||||
<button
|
||||
v-if="message.content"
|
||||
class="message-copy-button"
|
||||
type="button"
|
||||
:data-tooltip="isMessageCopied(message.id) ? t('conversation.copied') : t('conversation.copyMessage')"
|
||||
:aria-label="isMessageCopied(message.id) ? t('conversation.copied') : t('conversation.copyMessage')"
|
||||
@click.stop="copyMessageContent(message, $event, true)"
|
||||
@keydown.enter.prevent="copyMessageContent(message, $event, true)"
|
||||
@keydown.space.prevent="copyMessageContent(message, $event, true)"
|
||||
>
|
||||
<CheckmarkCircleOutline v-if="isMessageCopied(message.id)" />
|
||||
<CopyOutline v-else />
|
||||
</button>
|
||||
<button
|
||||
v-if="!message.provisional"
|
||||
class="message-delete-button"
|
||||
@@ -1059,14 +1059,20 @@ function humanGateSummary(gate) {
|
||||
'Cost usage is unavailable for this Agent attempt.': 'humanGate.summary.budget',
|
||||
'This run requires a human decision.': 'humanGate.summary.decision',
|
||||
'The previous write-capable Agent attempt may already have changed the workspace.': 'humanGate.summary.retry',
|
||||
'The candidate and unresolved issues have remained unchanged for two rounds.': 'humanGate.summary.stalledCandidate',
|
||||
'The workspace-write synthesis attempt may have produced side effects, but its result is unknown.': 'humanGate.summary.unknownWriteSynthesis',
|
||||
}[gate?.summary]
|
||||
return key ? t(key) : gate?.summary || ''
|
||||
}
|
||||
|
||||
function humanGateOptionLabel(option) {
|
||||
const optionIdKey = {
|
||||
'continue-discussion': 'humanGate.option.continueDiscussion',
|
||||
'stop-discussion': 'humanGate.option.stopDiscussion',
|
||||
'retry-once': 'humanGate.option.retryOnce',
|
||||
'cancel-retry': 'humanGate.option.cancelRetry',
|
||||
'retry-original-writer': 'humanGate.option.retryOriginalWriter',
|
||||
'replace-next-writer': 'humanGate.option.replaceNextWriter',
|
||||
}[option?.optionId]
|
||||
if (optionIdKey) return t(optionIdKey)
|
||||
const key = {
|
||||
|
||||
@@ -653,14 +653,20 @@ function humanGateSummary(gate) {
|
||||
'Cost usage is unavailable for this Agent attempt.': 'humanGate.summary.budget',
|
||||
'This run requires a human decision.': 'humanGate.summary.decision',
|
||||
'The previous write-capable Agent attempt may already have changed the workspace.': 'humanGate.summary.retry',
|
||||
'The candidate and unresolved issues have remained unchanged for two rounds.': 'humanGate.summary.stalledCandidate',
|
||||
'The workspace-write synthesis attempt may have produced side effects, but its result is unknown.': 'humanGate.summary.unknownWriteSynthesis',
|
||||
}[gate?.summary]
|
||||
return key ? t(key) : gate?.summary || ''
|
||||
}
|
||||
|
||||
function humanGateOptionLabel(option) {
|
||||
const optionIdKey = {
|
||||
'continue-discussion': 'humanGate.option.continueDiscussion',
|
||||
'stop-discussion': 'humanGate.option.stopDiscussion',
|
||||
'retry-once': 'humanGate.option.retryOnce',
|
||||
'cancel-retry': 'humanGate.option.cancelRetry',
|
||||
'retry-original-writer': 'humanGate.option.retryOriginalWriter',
|
||||
'replace-next-writer': 'humanGate.option.replaceNextWriter',
|
||||
}[option?.optionId]
|
||||
if (optionIdKey) return t(optionIdKey)
|
||||
const key = {
|
||||
|
||||
@@ -17,6 +17,10 @@ export function useConversationViewport({ activeMessages, liveOutputSignature, s
|
||||
await nextTick()
|
||||
const scroller = messageScroller.value
|
||||
if (!scroller || (!force && !messageNearBottom.value)) return
|
||||
if (force) {
|
||||
const container = scroller.closest?.('.conversation-pane, .workspace-pane')
|
||||
container?.scrollIntoView?.({ block: 'end', inline: 'nearest' })
|
||||
}
|
||||
scroller.scrollTop = scroller.scrollHeight
|
||||
messageNearBottom.value = true
|
||||
showScrollToLatest.value = false
|
||||
|
||||
@@ -39,8 +39,9 @@ const EVENT_STATUSES = new Set([
|
||||
'completed', 'succeeded', 'failed', 'cancelled', 'stopped', 'partial', 'timeout', 'interrupted',
|
||||
])
|
||||
const ORCHESTRATION_PHASES = new Set([
|
||||
'proposal', 'challenge', 'coordination', 'work', 'synthesis', 'verification', 'committed',
|
||||
'human-gate', 'stopped',
|
||||
'prepare', 'dispatch', 'running', 'reconcile',
|
||||
'proposal', 'challenge', 'coordination', 'work', 'synthesis', 'verification',
|
||||
'commit', 'committed', 'failed', 'stopped', 'human-gate',
|
||||
])
|
||||
const ORCHESTRATION_SLOT_STATUSES = new Set([
|
||||
'planned', 'prepared', 'queued', 'running', 'waiting', 'settled', 'completed', 'committed',
|
||||
|
||||
@@ -310,12 +310,18 @@ export default {
|
||||
'humanGate.summary.budget': 'This Agent cannot report cost usage for this attempt.',
|
||||
'humanGate.summary.decision': 'This run requires your decision.',
|
||||
'humanGate.summary.retry': 'The previous write-capable Agent attempt may already have changed the workspace.',
|
||||
'humanGate.summary.stalledCandidate': 'The candidate and unresolved issues have remained unchanged for two rounds.',
|
||||
'humanGate.summary.unknownWriteSynthesis': 'The workspace-write synthesis attempt may have produced side effects, but its result is unknown.',
|
||||
'humanGate.option.allowOnce': 'Allow once',
|
||||
'humanGate.option.allowAlways': 'Always allow',
|
||||
'humanGate.option.reject': 'Reject',
|
||||
'humanGate.option.rejectAlways': 'Always reject',
|
||||
'humanGate.option.retryOnce': 'Retry once',
|
||||
'humanGate.option.cancelRetry': 'Do not retry',
|
||||
'humanGate.option.continueDiscussion': 'Continue discussion',
|
||||
'humanGate.option.stopDiscussion': 'Stop discussion',
|
||||
'humanGate.option.retryOriginalWriter': 'Retry original writer',
|
||||
'humanGate.option.replaceNextWriter': 'Use next writer',
|
||||
'humanGate.option.acceptArtifact': 'Accept Artifact',
|
||||
'humanGate.option.rejectArtifact': 'Reject Artifact',
|
||||
'humanGate.option.reopenTask': 'Reopen Task',
|
||||
@@ -357,12 +363,18 @@ export default {
|
||||
'trace.eventConnectorLimited': 'Connector provided only the final answer; structured tool activity was unavailable.',
|
||||
'trace.eventOther': 'Event',
|
||||
'run.phase.proposal': 'Parallel proposals',
|
||||
'run.phase.prepare': 'Preparing orchestration',
|
||||
'run.phase.dispatch': 'Dispatching Agents',
|
||||
'run.phase.running': 'Running',
|
||||
'run.phase.reconcile': 'Reconciling results',
|
||||
'run.phase.challenge': 'Cross-agent challenge',
|
||||
'run.phase.coordination': 'Responsibility alignment',
|
||||
'run.phase.work': 'Collaborative execution',
|
||||
'run.phase.synthesis': 'Convergent synthesis',
|
||||
'run.phase.verification': 'Independent verification',
|
||||
'run.phase.commit': 'Committing delivery',
|
||||
'run.phase.committed': 'Delivered',
|
||||
'run.phase.failed': 'Failed',
|
||||
'run.phase.human-gate': 'Waiting for your decision',
|
||||
'run.phase.stopped': 'Stopped',
|
||||
'run.phase.unknown': 'Preparing',
|
||||
|
||||
@@ -310,12 +310,18 @@ export default {
|
||||
'humanGate.summary.budget': '该 Agent 本次尝试无法上报成本用量。',
|
||||
'humanGate.summary.decision': '该运行需要你做出决定。',
|
||||
'humanGate.summary.retry': '上一次可写 Agent 尝试可能已经修改工作区。',
|
||||
'humanGate.summary.stalledCandidate': '候选结论与未解决问题已连续两轮没有变化。',
|
||||
'humanGate.summary.unknownWriteSynthesis': '工作区写入综合尝试可能已产生副作用,但结果未知。',
|
||||
'humanGate.option.allowOnce': '允许一次',
|
||||
'humanGate.option.allowAlways': '始终允许',
|
||||
'humanGate.option.reject': '拒绝',
|
||||
'humanGate.option.rejectAlways': '始终拒绝',
|
||||
'humanGate.option.retryOnce': '重试一次',
|
||||
'humanGate.option.cancelRetry': '不重试',
|
||||
'humanGate.option.continueDiscussion': '继续讨论',
|
||||
'humanGate.option.stopDiscussion': '停止讨论',
|
||||
'humanGate.option.retryOriginalWriter': '重试原写入者',
|
||||
'humanGate.option.replaceNextWriter': '改由下一位写入者执行',
|
||||
'humanGate.option.acceptArtifact': '接受产物',
|
||||
'humanGate.option.rejectArtifact': '拒绝产物',
|
||||
'humanGate.option.reopenTask': '重新打开任务',
|
||||
@@ -357,12 +363,18 @@ export default {
|
||||
'trace.eventConnectorLimited': '连接器只提供最终答案,未暴露结构化工具过程。',
|
||||
'trace.eventOther': '事件',
|
||||
'run.phase.proposal': '并行提案',
|
||||
'run.phase.prepare': '准备编排',
|
||||
'run.phase.dispatch': '分发 Agent',
|
||||
'run.phase.running': '运行中',
|
||||
'run.phase.reconcile': '汇总结果',
|
||||
'run.phase.challenge': '交叉挑战',
|
||||
'run.phase.coordination': '职责协商',
|
||||
'run.phase.work': '协作执行',
|
||||
'run.phase.synthesis': '收敛整合',
|
||||
'run.phase.verification': '独立复核',
|
||||
'run.phase.commit': '提交交付结果',
|
||||
'run.phase.committed': '已交付',
|
||||
'run.phase.failed': '失败',
|
||||
'run.phase.human-gate': '等待你的决定',
|
||||
'run.phase.stopped': '已停止',
|
||||
'run.phase.unknown': '准备中',
|
||||
|
||||
@@ -615,7 +615,6 @@
|
||||
}
|
||||
|
||||
.message-row.agent-reply-collapsed .message-footer-actions > .message-regenerate-button,
|
||||
.message-row.agent-reply-collapsed .message-footer-actions > .message-copy-button,
|
||||
.message-row.agent-reply-collapsed .message-footer-actions > .message-delete-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,11 @@
|
||||
.onboarding-dots { justify-content: center; }
|
||||
.onboarding-primary { width: 100%; min-width: 0; }
|
||||
.app-shell,
|
||||
.app-shell.sidebar-collapsed { grid-template-columns: 1fr; }
|
||||
.app-shell.sidebar-collapsed {
|
||||
grid-template-columns: 1fr;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.app-shell.trace-panel-open,
|
||||
.app-shell.sidebar-collapsed.trace-panel-open { grid-template-columns: 1fr; }
|
||||
.sidebar { height: auto; min-height: 48dvh; border-right: 0; border-bottom: 0; }
|
||||
|
||||
Reference in new issue
Block a user